Upgrade to Agsv2 + Astal (#533)
* migrate to astal * Reorganize project structure. * progress * Migrate Dashboard and Window Title modules. * Migrate clock and notification bar modules. * Remove unused code * Media menu * Rework network and volume modules * Finish custom modules. * Migrate battery bar module. * Update battery module and organize helpers. * Migrate workspace module. * Wrap up bar modules. * Checkpoint before I inevitbly blow something up. * Updates * Fix event propagation logic. * Type fixes * More type fixes * Fix padding for event boxes. * Migrate volume menu and refactor scroll event handlers. * network module WIP * Migrate network service. * Migrate bluetooth menu * Updates * Migrate notifications * Update scrolling behavior for custom modules. * Improve popup notifications and add timer functionality. * Migration notifications menu header/controls. * Migrate notifications menu and consolidate notifications menu code. * Migrate power menu. * Dashboard progress * Migrate dashboard * Migrate media menu. * Reduce media menu nesting. * Finish updating media menu bindings to navigate active player. * Migrate battery menu * Consolidate code * Migrate calendar menu * Fix workspace logic to update on client add/change/remove and consolidate code. * Migrate osd * Consolidate hyprland service connections. * Implement startup dropdown menu position allocation. * Migrate settings menu (WIP) * Settings dialo menu fixes * Finish Dashboard menu * Type updates * update submoldule for types * update github ci * ci * Submodule update * Ci updates * Remove type checking for now. * ci fix * Fix a bunch of stuff, losing track... need rest. Brb coffee * Validate dropdown menu before render. * Consolidate code and add auto-hide functionality. * Improve auto-hide behavior. * Consolidate audio menu code * Organize bluetooth code * Improve active player logic * Properly dismiss a notification on action button resolution. * Implement CLI command engine and migrate CLI commands. * Handle variable disposal * Bar component fixes and add hyprland startup rules. * Handle potentially null bindings network and bluetooth bindings. * Handle potentially null wired adapter. * Fix GPU stats * Handle poller for GPU * Fix gpu bar logic. * Clean up logic for stat bars. * Handle wifi and wired bar icon bindings. * Fix battery percentages * Fix switch behavior * Wifi staging fixes * Reduce redundant hyprland service calls. * Code cleanup * Document the option code and reduce redundant calls to optimize performance. * Remove outdated comment. * Add JSDocs * Add meson to build hyprpanel * Consistency updates * Organize commands * Fix images not showing up on notifications. * Remove todo * Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands. * Handle SRC directory for the bundled/built hyprpanel. * Add namespaces to all windows * Migrate systray * systray updates * Update meson to include ts, tsx and scss files. * Remove log from meson * Fix file choose path and make it float. * Added a command to check the dependency status * Update dep names. * Get scale directly from env * Add todo
This commit is contained in:
61
src/components/notifications/Actions.tsx
Normal file
61
src/components/notifications/Actions.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { execAsync } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
|
||||
const ActionButton = ({ notification, action }: ActionButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={'notification-action-buttons'}
|
||||
hexpand
|
||||
onClick={(_, event) => {
|
||||
if (!isPrimaryClick(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.id.includes('scriptAction:-')) {
|
||||
execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) => console.error(err));
|
||||
notification.dismiss();
|
||||
} else {
|
||||
notification.invoke(action.id);
|
||||
notification.dismiss();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box halign={Gtk.Align.CENTER} hexpand>
|
||||
<label
|
||||
className={'notification-action-buttons-label'}
|
||||
label={action.label}
|
||||
hexpand
|
||||
max_width_chars={15}
|
||||
truncate
|
||||
wrap
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const Actions = ({ notification, showActions }: ActionProps): JSX.Element => {
|
||||
return (
|
||||
<revealer transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} revealChild={showActions ? false : true}>
|
||||
<eventbox>
|
||||
<box className={'notification-card-actions'} hexpand valign={Gtk.Align.END}>
|
||||
{notification.get_actions().map((action) => {
|
||||
return <ActionButton notification={notification} action={action} />;
|
||||
})}
|
||||
</box>
|
||||
</eventbox>
|
||||
</revealer>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActionProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
showActions: boolean;
|
||||
}
|
||||
|
||||
interface ActionButtonProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
action: AstalNotifd.Action;
|
||||
}
|
||||
27
src/components/notifications/Body.tsx
Normal file
27
src/components/notifications/Body.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { notifHasImg } from './helpers';
|
||||
|
||||
export const Body = ({ notification }: BodyProps): JSX.Element => {
|
||||
return (
|
||||
<box className={'notification-card-body'} valign={Gtk.Align.START} hexpand>
|
||||
<label
|
||||
className={'notification-card-body-label'}
|
||||
halign={Gtk.Align.START}
|
||||
label={notification.body}
|
||||
maxWidthChars={!notifHasImg(notification) ? 35 : 28}
|
||||
lines={2}
|
||||
truncate
|
||||
wrap
|
||||
justify={Gtk.Justification.LEFT}
|
||||
hexpand
|
||||
useMarkup
|
||||
onRealize={(self) => self.set_markup(notification.body)}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface BodyProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
19
src/components/notifications/CloseButton.tsx
Normal file
19
src/components/notifications/CloseButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
|
||||
export const CloseButton = ({ notification }: CloseButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={'close-notification-button'}
|
||||
onClick={() => {
|
||||
notification.dismiss();
|
||||
}}
|
||||
>
|
||||
<label className={'txt-icon notification-close'} label={''} halign={Gtk.Align.CENTER}></label>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface CloseButtonProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
64
src/components/notifications/Header.tsx
Normal file
64
src/components/notifications/Header.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import options from 'src/options.js';
|
||||
import { GLib } from 'astal/gobject.js';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { getNotificationIcon } from 'src/globals/notification.js';
|
||||
import { notifHasImg } from './helpers';
|
||||
|
||||
const { military } = options.menus.clock.time;
|
||||
|
||||
export const NotificationIcon = ({ notification }: HeaderProps): JSX.Element => {
|
||||
const { appName, appIcon, desktopEntry } = notification;
|
||||
|
||||
return (
|
||||
<box className={'notification-card-header'} halign={Gtk.Align.START}>
|
||||
<box css={'min-width: 2rem; min-height: 2rem; '}>
|
||||
<icon className={'notification-icon'} icon={getNotificationIcon(appName, appIcon, desktopEntry)} />
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
export const SummaryLabel = ({ notification }: HeaderProps): JSX.Element => {
|
||||
return (
|
||||
<box className={'notification-card-header'} halign={Gtk.Align.START} valign={Gtk.Align.START} hexpand>
|
||||
<label
|
||||
className={'notification-card-header-label'}
|
||||
halign={Gtk.Align.START}
|
||||
onRealize={(self) => self.set_markup(notification.summary)}
|
||||
label={notification.summary}
|
||||
maxWidthChars={!notifHasImg(notification) ? 30 : 19}
|
||||
hexpand
|
||||
vexpand
|
||||
truncate
|
||||
wrap
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimeLabel = ({ notification }: HeaderProps): JSX.Element => {
|
||||
const time = (time: number, format = '%I:%M %p'): string => {
|
||||
return GLib.DateTime.new_from_unix_local(time).format(military.get() ? '%H:%M' : format) || '--';
|
||||
};
|
||||
|
||||
return (
|
||||
<box className={'notification-card-header menu'} halign={Gtk.Align.END} valign={Gtk.Align.START} hexpand>
|
||||
<label className={'notification-time'} label={time(notification.time)} vexpand />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = ({ notification }: HeaderProps): JSX.Element => {
|
||||
return (
|
||||
<box vertical={false} hexpand>
|
||||
<NotificationIcon notification={notification} />
|
||||
<SummaryLabel notification={notification} />
|
||||
<TimeLabel notification={notification} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
48
src/components/notifications/Image.tsx
Normal file
48
src/components/notifications/Image.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { notifHasImg } from './helpers';
|
||||
import { isAnImage } from 'src/lib/utils';
|
||||
|
||||
const ImageItem = ({ notification }: ImageProps): JSX.Element => {
|
||||
if (notification.appIcon && !isAnImage(notification.appIcon)) {
|
||||
return (
|
||||
<icon
|
||||
className={'notification-card-image icon'}
|
||||
halign={Gtk.Align.CENTER}
|
||||
vexpand={false}
|
||||
icon={notification.appIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
className={'notification-card-image'}
|
||||
halign={Gtk.Align.CENTER}
|
||||
vexpand={false}
|
||||
css={`
|
||||
background-image: url('${notification.image || notification.appIcon}');
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const Image = ({ notification }: ImageProps): JSX.Element => {
|
||||
if (!notifHasImg(notification)) {
|
||||
return <box />;
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
className={'notification-card-image-container'}
|
||||
halign={Gtk.Align.CENTER}
|
||||
valign={Gtk.Align.CENTER}
|
||||
vexpand={false}
|
||||
>
|
||||
<ImageItem notification={notification} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ImageProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
65
src/components/notifications/Notification.tsx
Normal file
65
src/components/notifications/Notification.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Actions } from './Actions';
|
||||
import { Body } from './Body';
|
||||
import { CloseButton } from './CloseButton';
|
||||
import { Header } from './Header';
|
||||
import { Image } from './Image';
|
||||
import { Gtk, Widget } from 'astal/gtk3';
|
||||
import { isSecondaryClick } from 'src/lib/utils';
|
||||
import { notifHasImg } from './helpers';
|
||||
|
||||
const NotificationContent = ({ actionBox, notification }: NotificationContentProps): JSX.Element => {
|
||||
return (
|
||||
<box className={`notification-card-content ${!notifHasImg(notification) ? 'noimg' : ''}`} hexpand vertical>
|
||||
<Header notification={notification} />
|
||||
<Body notification={notification} />
|
||||
{actionBox}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotificationCard = ({ notification, showActions, ...props }: NotificationCardProps): JSX.Element => {
|
||||
const actionBox: IActionBox | null = notification.get_actions().length ? (
|
||||
<Actions notification={notification} showActions={showActions} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<eventbox
|
||||
onClick={(_, event) => {
|
||||
if (isSecondaryClick(event)) {
|
||||
notification.dismiss();
|
||||
}
|
||||
}}
|
||||
onHover={() => {
|
||||
if (actionBox !== null && showActions === true) {
|
||||
actionBox.revealChild = true;
|
||||
}
|
||||
}}
|
||||
onHoverLost={() => {
|
||||
if (actionBox !== null && showActions === true) {
|
||||
actionBox.revealChild = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box className={'notification-card'} {...props} hexpand valign={Gtk.Align.START}>
|
||||
<Image notification={notification} />
|
||||
<NotificationContent notification={notification} actionBox={actionBox} />
|
||||
<CloseButton notification={notification} />
|
||||
</box>
|
||||
</eventbox>
|
||||
);
|
||||
};
|
||||
|
||||
interface NotificationCardProps extends Widget.BoxProps {
|
||||
notification: AstalNotifd.Notification;
|
||||
showActions: boolean;
|
||||
}
|
||||
|
||||
interface IActionBox extends Gtk.Widget {
|
||||
revealChild?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationContentProps {
|
||||
actionBox: IActionBox | null;
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
87
src/components/notifications/helpers.ts
Normal file
87
src/components/notifications/helpers.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { bind, timeout, Variable } from 'astal';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import options from 'src/options';
|
||||
import { hyprlandService, notifdService } from 'src/lib/constants/services';
|
||||
import { isNotificationIgnored } from 'src/lib/shared/notifications';
|
||||
|
||||
const { ignore, timeout: popupTimeout } = options.notifications;
|
||||
|
||||
/**
|
||||
* Checks if a notification has an image.
|
||||
*
|
||||
* This function determines whether the provided notification contains an image by checking the `image` property.
|
||||
*
|
||||
* @param notification The notification object to check.
|
||||
*
|
||||
* @returns True if the notification has an image, false otherwise.
|
||||
*/
|
||||
export const notifHasImg = (notification: AstalNotifd.Notification): boolean => {
|
||||
return (notification.image && notification.image.length) || notification.appIcon ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the active monitor and updates the provided variable.
|
||||
*
|
||||
* This function sets up a derived variable that updates the `curMonitor` variable with the ID of the focused monitor.
|
||||
*
|
||||
* @param curMonitor The variable to update with the active monitor ID.
|
||||
*/
|
||||
export const trackActiveMonitor = (curMonitor: Variable<number>): void => {
|
||||
Variable.derive([bind(hyprlandService, 'focusedMonitor')], (monitor) => {
|
||||
curMonitor.set(monitor.id);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks popup notifications and updates the provided variable.
|
||||
*
|
||||
* This function connects to the `notified` and `resolved` signals of the `notifdService` to manage popup notifications.
|
||||
* It updates the `popupNotifications` variable with the current list of notifications and handles dismissing notifications based on the timeout.
|
||||
*
|
||||
* @param popupNotifications The variable to update with the list of popup notifications.
|
||||
*/
|
||||
export const trackPopupNotifications = (popupNotifications: Variable<AstalNotifd.Notification[]>): void => {
|
||||
notifdService.connect('notified', (_, id) => {
|
||||
const notification = notifdService.get_notification(id);
|
||||
const doNotDisturb = notifdService.dontDisturb;
|
||||
|
||||
if (isNotificationIgnored(notification, ignore.get())) {
|
||||
notification.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
if (doNotDisturb) {
|
||||
return;
|
||||
}
|
||||
|
||||
popupNotifications.set([...popupNotifications.get(), notification]);
|
||||
|
||||
timeout(popupTimeout.get(), () => {
|
||||
dropNotificationPopup(notification, popupNotifications);
|
||||
});
|
||||
});
|
||||
|
||||
notifdService.connect('resolved', (_, id) => {
|
||||
const filteredPopups = popupNotifications.get().filter((popupNotif) => popupNotif.id !== id);
|
||||
|
||||
popupNotifications.set(filteredPopups);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dismisses a notification popup and updates the provided variable.
|
||||
*
|
||||
* This function removes the specified notification from the list of popup notifications and updates the `popupNotifications` variable.
|
||||
*
|
||||
* @param notificationToDismiss The notification to dismiss.
|
||||
* @param popupNotifications The variable to update with the list of popup notifications.
|
||||
*/
|
||||
const dropNotificationPopup = (
|
||||
notificationToDismiss: AstalNotifd.Notification,
|
||||
popupNotifications: Variable<AstalNotifd.Notification[]>,
|
||||
): void => {
|
||||
const currentPopups = popupNotifications.get();
|
||||
const undismissedNotifications = currentPopups.filter((popupNotif) => popupNotif.id !== notificationToDismiss.id);
|
||||
|
||||
popupNotifications.set(undismissedNotifications);
|
||||
};
|
||||
63
src/components/notifications/index.tsx
Normal file
63
src/components/notifications/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services.js';
|
||||
import options from 'src/options.js';
|
||||
import { getPosition } from 'src/lib/utils.js';
|
||||
import Variable from 'astal/variable.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { trackActiveMonitor, trackPopupNotifications } from './helpers.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { NotificationCard } from './Notification.js';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
|
||||
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
||||
const { tear } = options;
|
||||
|
||||
const curMonitor = Variable(monitor.get());
|
||||
const popupNotifications: Variable<AstalNotifd.Notification[]> = Variable([]);
|
||||
|
||||
trackActiveMonitor(curMonitor);
|
||||
trackPopupNotifications(popupNotifications);
|
||||
|
||||
export default (): JSX.Element => {
|
||||
const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
|
||||
const windowAnchor = bind(position).as(getPosition);
|
||||
const windowMonitor = Variable.derive(
|
||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||
(focusedMonitor, monitor, activeMonitor) => {
|
||||
if (activeMonitor === true) {
|
||||
return focusedMonitor.id;
|
||||
}
|
||||
return monitor;
|
||||
},
|
||||
);
|
||||
|
||||
const notificationsBinding = Variable.derive(
|
||||
[bind(popupNotifications), bind(showActionsOnHover)],
|
||||
(notifications, showActions) => {
|
||||
const maxDisplayed = notifications.slice(0, displayedTotal.get());
|
||||
|
||||
return maxDisplayed.map((notification) => {
|
||||
return <NotificationCard notification={notification} showActions={showActions} />;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<window
|
||||
name={'notifications-window'}
|
||||
namespace={'notifications-window'}
|
||||
className={'notifications-window'}
|
||||
layer={windowLayer}
|
||||
anchor={windowAnchor}
|
||||
exclusivity={Astal.Exclusivity.NORMAL}
|
||||
monitor={windowMonitor()}
|
||||
onDestroy={() => {
|
||||
windowMonitor.drop();
|
||||
notificationsBinding.drop();
|
||||
}}
|
||||
>
|
||||
<box vertical hexpand className={'notification-card-container'}>
|
||||
{notificationsBinding()}
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user