From 12e4cf58b60b0d83774eb109ff70a62acbd23623 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Tue, 11 Mar 2025 16:16:07 -0300 Subject: [PATCH] :sparkles: feat(ags/notifications): add support to actions in floating notifications(popups) --- ags/scripts/notifications.ts | 8 ++- ags/scripts/recording.ts | 81 +++++++++++----------- ags/style.scss | 31 ++++++++- ags/style/_colors.scss | 1 + ags/style/_control-center.scss | 5 ++ ags/style/_float-notifications.scss | 2 +- ags/style/_mixins.scss | 2 + ags/widget/Notification.ts | 16 +++++ ags/widget/PopupWindow.ts | 2 +- ags/widget/control-center/NotifHistory.ts | 64 ++--------------- ags/widget/control-center/pages/Network.ts | 55 ++++++++++++++- ags/window/Runner.ts | 4 +- 12 files changed, 162 insertions(+), 109 deletions(-) diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts index 4aac42c..4e6f1e5 100644 --- a/ags/scripts/notifications.ts +++ b/ags/scripts/notifications.ts @@ -125,11 +125,13 @@ class Notifications extends GObject.Object { } public removeNotification(notif: (AstalNotifd.Notification|number)): void { - const notifId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; + const notification = (notif instanceof AstalNotifd.Notification) ? notif : AstalNotifd.get_default().get_notification(notif); this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) => - item.id !== notifId); + item.id !== notification.id); + + notification.dismiss(); this.notify("notifications"); - this.emit("notification-removed", notifId); + this.emit("notification-removed", notification.id); } connect(signal: string, callback: (...args: any[]) => void): number { diff --git a/ags/scripts/recording.ts b/ags/scripts/recording.ts index 9491fe6..97ba291 100644 --- a/ags/scripts/recording.ts +++ b/ags/scripts/recording.ts @@ -1,11 +1,10 @@ -import { execAsync, GLib, GObject, register, signal, writeFile } from "astal"; -import { Subscribable } from "astal/binding"; +import { execAsync, GLib, GObject, property, register, signal } from "astal"; +import { Connectable } from "astal/binding"; import { Gdk } from "astal/gtk3"; import { getDateTime } from "./time"; -import AstalWp from "gi://AstalWp"; @register({ GTypeName: "ScreenRecording" }) -class Recording extends GObject.Object implements Subscribable { +class Recording extends GObject.Object implements Connectable { private static instance: Recording; @@ -17,27 +16,44 @@ class Recording extends GObject.Object implements Subscribable { declare outputChanged: (newPath: string) => void; #recording: boolean = false; - #subs = new Set<(isRec: boolean) => void>(); #path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`; /** Default extension: mp4(h264) */ #extension: string = "mp4"; - #recordAudio: boolean|AstalWp.Endpoint = false; // TODO + #recordAudio: boolean = false; + #monitor: (number|null) = null; + #area: (Gdk.Rectangle|null) = null; - private notifySub() { - const subs = this.#subs; - for(const sub of subs) { - sub(this.recording); - } + @property(Boolean) + public get recording() { return this.#recording; } + private set recording(newValue: boolean) { + (!newValue && this.recording) ? + this.stopRecording() + : this.startRecording(this.#monitor || 0, this.#area || undefined); + + this.#recording = newValue; + this.notify("recording"); } - public get recording() { return this.#recording; } - private set recording(newValue: boolean) { this.#recording = newValue; } - + @property(String) public get path() { return this.#path; } - public set path(newPath: string) { this.#path = newPath; } + public set path(newPath: string) { + this.#path = newPath; + this.notify("path"); + } + @property(String) public get extension() { return this.#extension; } - public set extension(newExt: string) { this.#extension = newExt; } + public set extension(newExt: string) { + this.#extension = newExt; + this.notify("extension"); + } + + @property(Boolean) + public get recordAudio() { return this.#recordAudio; } + public set recordAudio(newValue: boolean) { + this.#recordAudio = newValue; + this.notify("record-audio"); + } constructor() { super(); @@ -50,39 +66,26 @@ class Recording extends GObject.Object implements Subscribable { return this.instance; } - public get() { - return this.recording; - } - - private emit(id: string, ...args: any[]) { - super.emit(id, ...args); - this.notifySub(); - } - - - public startRecording(area?: Gdk.Rectangle) { + public startRecording(monitor?: number, area?: Gdk.Rectangle) { const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`; - execAsync([ "wf-recorder", + this.#recording = true; + this.emit("started"); + execAsync([ + "wf-recorder", `${Boolean(area) ? `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` - : ""}`, - "-f", output ] - ).then(() => { + : ""}`, + `-f ${output}` + ]).then(() => { this.emit("stopped", `${this.path}/${output}`); + this.#recording = false; + this.notify("recording"); }); - writeFile("", ""); - this.emit("started"); - this.notifySub(); } public stopRecording() { } - - public subscribe(callback: (isRec: boolean) => void) { - this.#subs.add(callback); - return () => this.#subs.delete(callback); - } } export { Recording }; diff --git a/ags/style.scss b/ags/style.scss index 6c66762..e68ad10 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -57,7 +57,7 @@ window.ask-popup { } .notification { - background: colors.$bg-primary; + background: colors.$bg-translucent-secondary; border-radius: 16px; & > .top { @@ -87,8 +87,9 @@ window.ask-popup { } & .content { - padding: 4px; + padding: 6px; padding-top: 0; + & .image { $size: 78px; min-width: $size; @@ -110,6 +111,32 @@ window.ask-popup { font-weight: 400; } } + + & .actions { + padding: 6px; + + & button.action { + @include mixins.hover-shadow; + + border-radius: 4px; + background: colors.$bg-secondary; + padding: 6px; + + & label { + font-size: 14px; + font-weight: 600; + } + + &:first-child { + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; + } + &:last-child { + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; + } + } + } } tooltip { diff --git a/ags/style/_colors.scss b/ags/style/_colors.scss index b3f2f6c..c81590f 100644 --- a/ags/style/_colors.scss +++ b/ags/style/_colors.scss @@ -7,6 +7,7 @@ $bg-secondary: funs.toRGB(color.adjust($color: wal.$color1, $lightness: -16%)); $bg-tertiary: funs.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%)); $bg-light: wal.$foreground; $bg-translucent: funs.toRGB(color.change($color: $bg-primary, $alpha: 75%)); +$bg-translucent-secondary: funs.toRGB(color.change($color: $bg-translucent, $alpha: 78%)); $fg-primary: wal.$foreground; $fg-light: $bg-primary; $fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%)); diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 1425d8b..9c9edc0 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -145,6 +145,11 @@ } } + & .sub-header { + font-size: 18px; + font-weight: 500; + } + &.bluetooth { .connections button { @include mixins.hover-shadow; diff --git a/ags/style/_float-notifications.scss b/ags/style/_float-notifications.scss index 6f789bb..a633144 100644 --- a/ags/style/_float-notifications.scss +++ b/ags/style/_float-notifications.scss @@ -9,6 +9,6 @@ & > .notification { margin: 6px; - box-shadow: 0 0 4px .5px colors.$bg-translucent; + box-shadow: 0 0 4px .5px colors.$bg-primary; } } diff --git a/ags/style/_mixins.scss b/ags/style/_mixins.scss index f3ad84e..ce784f8 100644 --- a/ags/style/_mixins.scss +++ b/ags/style/_mixins.scss @@ -35,11 +35,13 @@ &:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; + margin-left: 0; } &:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; + margin-right: 0; } } } diff --git a/ags/widget/Notification.ts b/ags/widget/Notification.ts index c1b2c5c..80d6e18 100644 --- a/ags/widget/Notification.ts +++ b/ags/widget/Notification.ts @@ -101,6 +101,22 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number ] } as Widget.BoxProps) ] + } as Widget.BoxProps), + new Widget.Box({ + className: "actions button-row", + hexpand: true, + visible: notification.actions.length > 0, + children: notification.actions.map((action: AstalNotifd.Action) => + new Widget.Button({ + className: "action", + label: action.label, + hexpand: true, + onClicked: () => { + notification.invoke(action.id); + onClose && onClose(notification); + } + } as Widget.ButtonProps) + ) } as Widget.BoxProps) ] } as Widget.BoxProps) diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index 38ad9af..de05059 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -74,8 +74,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { widthRequest: props?.widthRequest, heightRequest: props?.heightRequest, hexpand: props?.hexpand || false, - visible: true, vexpand: props?.vexpand || false, + visible: true, css: `.popup { margin-top: ${props.marginTop || 0}px; margin-bottom: ${props.marginBottom || 0}px; diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts index 848c7a1..1a6ccd2 100644 --- a/ags/widget/control-center/NotifHistory.ts +++ b/ags/widget/control-center/NotifHistory.ts @@ -2,6 +2,7 @@ import { bind } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalNotifd from "gi://AstalNotifd"; import { Notifications } from "../../scripts/notifications"; +import { NotificationWidget } from "../Notification"; export const NotifHistory: Gtk.Widget = new Widget.Scrollable({ hscroll: Gtk.PolicyType.NEVER, @@ -10,65 +11,8 @@ export const NotifHistory: Gtk.Widget = new Widget.Scrollable({ child: new Widget.Box({ className: "notifications", children: bind(Notifications.getDefault(), "history").as((history: Array) => - history.map((notification: AstalNotifd.Notification) => - new Widget.Box({ - className: "notification", - hexpand: true, - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Box({ - className: "top", - expand: true, - children: [ - new Widget.Box({ - className: "app", - children: [ - new Widget.Icon({ - icon: notification.appIcon || notification.appName.toLowerCase(), - iconSize: Gtk.IconSize.LARGE_TOOLBAR - }), - new Widget.Label({ - className: "name", - label: notification.appName || "Unknown" - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Button({ - className: "remove", - label: "󱎘", - onClick: () => Notifications.getDefault().removeHistory(notification.id) - } as Widget.ButtonProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "content", - expand: true, - children: [ - new Widget.Box({ - className: "image", - visible: notification.image !== "", - css: `.image { background-image: url('${notification.image}') }` - } as Widget.BoxProps), - new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Label({ - className: "summary", - useMarkup: true, - label: notification.summary - } as Widget.LabelProps), - new Widget.Label({ - className: "body", - useMarkup: true, - label: notification.body - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - ) - ) + history.map((notification: AstalNotifd.Notification) => NotificationWidget(notification, + () => Notifications.getDefault().removeHistory(notification.id)) + )) } as Widget.BoxProps) } as Widget.ScrollableProps) diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts index 28fa373..36c2e6e 100644 --- a/ags/widget/control-center/pages/Network.ts +++ b/ags/widget/control-center/pages/Network.ts @@ -1,4 +1,4 @@ -import { Widget } from "astal/gtk3"; +import { Gtk, Widget } from "astal/gtk3"; import { Page } from "./Page"; import AstalNetwork from "gi://AstalNetwork"; import { bind } from "astal"; @@ -18,6 +18,59 @@ export const PageNetwork = new Page({ } as Widget.ButtonProps) ], pageChild: () => new Widget.Box({ + expand: true, + children: [ + new Widget.Box({ + className: "devices", + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0), + children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => [ + new Widget.Label({ + label: "Devices", + xalign: 0, + className: "sub-header", + } as Widget.LabelProps), + ...devices.map(dev => new Widget.Button({ + className: "device", + child: new Widget.Label({ + className: "interface name", + xalign: 0, + label: dev.interface + } as Widget.LabelProps), + } as Widget.ButtonProps)) + ]) + } as Widget.BoxProps), + new Widget.Box({ + className: "wireless-aps", + visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI), + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) => + aps.map(ap => new Widget.Button({ + hexpand: true, + onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :( + child: new Widget.Box({ + hexpand: true, + children: [ + new Widget.Icon({ + halign: Gtk.Align.START, + className: "icon", + icon: "network-wireless-signal-excellent-symbolic" + } as Widget.IconProps), + new Widget.Label({ + className: "ssid", + halign: Gtk.Align.START, + label: ap.ssid.toArray().toString() + } as Widget.LabelProps), + new Widget.Label({ + className: "status", + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps))) : [], + } as Widget.BoxProps), + ] } as Widget.BoxProps) }); diff --git a/ags/window/Runner.ts b/ags/window/Runner.ts index 6699b81..37a6e43 100644 --- a/ags/window/Runner.ts +++ b/ags/window/Runner.ts @@ -47,7 +47,7 @@ export namespace Runner { width?: number; height?: number; entryPlaceHolder?: string; - resultsPlaceholder?: () => Array; + resultsPlaceholder?: () => Array; }; export const prefixes = new Map (ResultWidget|Array|null)>([ @@ -58,7 +58,7 @@ export namespace Runner { export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) { let subs: Array<() => void> = []; const entryText: Variable = new Variable(""); - let results: (Array|null) = null; + let results: (Array|null) = props?.resultsPlaceholder ? props.resultsPlaceholder() : null; let selectedResultIndex = 0; const searchEntry = new Widget.Entry({