diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-16 16:35:37 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-16 16:35:37 +1100 |
| commit | 02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38 (patch) | |
| tree | 5e2a56becf6ba6961995e541ce9688224f704773 /src/widgets | |
| parent | popupwindow: switch to class (diff) | |
| download | caelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.tar.gz caelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.tar.bz2 caelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.zip | |
refactor: move ts to src
Also move popupwindow to own file
Diffstat (limited to 'src/widgets')
| -rw-r--r-- | src/widgets/notification.tsx | 132 | ||||
| -rw-r--r-- | src/widgets/popupwindow.ts | 39 |
2 files changed, 171 insertions, 0 deletions
diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx new file mode 100644 index 0000000..0bef5ca --- /dev/null +++ b/src/widgets/notification.tsx @@ -0,0 +1,132 @@ +import { GLib, register, timeout } from "astal"; +import { Astal, Gtk, Widget } from "astal/gtk3"; +import AstalNotifd from "gi://AstalNotifd"; +import { notifpopups as config } from "../../config"; +import { desktopEntrySubs } from "../utils/icons"; + +const urgencyToString = (urgency: AstalNotifd.Urgency) => { + switch (urgency) { + case AstalNotifd.Urgency.LOW: + return "low"; + case AstalNotifd.Urgency.NORMAL: + return "normal"; + case AstalNotifd.Urgency.CRITICAL: + return "critical"; + } +}; + +const getTime = (time: number) => { + const messageTime = GLib.DateTime.new_from_unix_local(time); + const todayDay = GLib.DateTime.new_now_local().get_day_of_year(); + if (messageTime.get_day_of_year() === todayDay) { + const aMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60); + return aMinuteAgo !== null && messageTime.compare(aMinuteAgo) > 0 ? "Now" : messageTime.format("%H:%M"); + } else if (messageTime.get_day_of_year() === todayDay - 1) return "Yesterday"; + return messageTime.format("%d/%m"); +}; + +const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: string }) => { + // Try app icon + let icon = Astal.Icon.lookup_icon(appIcon) && appIcon; + // Try desktop entry + if (!icon) { + if (desktopEntrySubs.hasOwnProperty(desktopEntry)) icon = desktopEntrySubs[desktopEntry]; + else if (Astal.Icon.lookup_icon(desktopEntry)) icon = desktopEntry; + } + return icon ? <icon className="app-icon" icon={icon} /> : null; +}; + +const Image = ({ icon }: { icon: string }) => { + if (GLib.file_test(icon, GLib.FileTest.EXISTS)) + return ( + <box + valign={Gtk.Align.START} + className="image" + css={` + background-image: url("${icon}"); + `} + /> + ); + if (Astal.Icon.lookup_icon(icon)) return <icon valign={Gtk.Align.START} className="image" icon={icon} />; + return null; +}; + +@register() +export default class Notification extends Widget.Box { + readonly #revealer; + #destroyed = false; + + constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) { + super(); + + this.#revealer = ( + <revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}> + <box className="wrapper"> + <box vertical className={`notification ${urgencyToString(notification.urgency)}`}> + <box className="header"> + <AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} /> + <label className="app-name" label={notification.appName ?? "Unknown"} /> + <box hexpand /> + <label + className="time" + label={getTime(notification.time)!} + setup={self => + timeout(60000, () => !this.#destroyed && (self.label = getTime(notification.time)!)) + } + /> + </box> + <box hexpand className="separator" /> + <box className="content"> + {notification.image && <Image icon={notification.image} />} + <box vertical> + <label className="summary" xalign={0} label={notification.summary} truncate /> + <label className="body" xalign={0} label={notification.body} wrap useMarkup /> + </box> + </box> + <box className="actions"> + <button hexpand cursor="pointer" onClicked={() => notification.dismiss()} label="Close" /> + {notification.actions.map(a => ( + <button hexpand cursor="pointer" onClicked={() => notification.invoke(a.id)}> + {notification.actionIcons ? <icon icon={a.label} /> : a.label} + </button> + ))} + </box> + </box> + </box> + </revealer> + ) as Widget.Revealer; + this.add(this.#revealer); + + // Init animation + const width = this.get_preferred_width()[1]; + this.css = `margin-left: ${width}px; margin-right: -${width}px;`; + timeout(1, () => { + this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`; + }); + + // Close popup after timeout if transient or expire enabled in config + if (popup && (config.expire || notification.transient)) + timeout( + notification.expireTimeout > 0 + ? notification.expireTimeout + : notification.urgency === AstalNotifd.Urgency.CRITICAL + ? 10000 + : 5000, + () => this.destroyWithAnims() + ); + } + + destroyWithAnims() { + if (this.#destroyed) return; + this.#destroyed = true; + + const animTime = 120; + const animMargin = this.get_allocated_width(); + this.css = `transition: ${animTime}ms cubic-bezier(0.85, 0, 0.15, 1); + margin-left: ${animMargin}px; margin-right: -${animMargin}px;`; + timeout(animTime, () => { + this.#revealer.revealChild = false; + timeout(this.#revealer.transitionDuration, () => this.destroy()); + }); + } +} diff --git a/src/widgets/popupwindow.ts b/src/widgets/popupwindow.ts new file mode 100644 index 0000000..67aa0ff --- /dev/null +++ b/src/widgets/popupwindow.ts @@ -0,0 +1,39 @@ +import { Binding, register } from "astal"; +import { App, Astal, Gdk, Widget } from "astal/gtk3"; +import AstalHyprland from "gi://AstalHyprland?version=0.1"; + +const extendProp = <T>( + prop: T | Binding<T | undefined> | undefined, + override: (prop: T | undefined) => T | undefined +) => prop && (prop instanceof Binding ? prop.as(override) : override(prop)); + +@register() +export default class PopupWindow extends Widget.Window { + constructor(props: Widget.WindowProps) { + super({ + keymode: Astal.Keymode.ON_DEMAND, + exclusivity: Astal.Exclusivity.IGNORE, + ...props, + visible: false, + application: App, + name: props.monitor ? extendProp(props.name, n => (n ? n + props.monitor : undefined)) : props.name, + namespace: extendProp(props.name, n => `caelestia-${n}`), + onKeyPressEvent: (self, event) => { + // Close window on escape + if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide(); + + return props.onKeyPressEvent?.(self, event); + }, + borderWidth: 20, // To allow shadow, cause if not it gets cut off + }); + } + + popup_at_widget(widget: JSX.Element, event: Gdk.Event) { + const { width, height } = widget.get_allocation(); + const [_, x, y] = event.get_coords(); + const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position(); + this.marginLeft = cx + ((width - this.get_preferred_width()[1]) / 2 - x); + this.marginTop = cy + (height - y); + this.show(); + } +} |