feat(ags/notifications): add support to actions in floating notifications(popups)

This commit is contained in:
retrozinndev
2025-03-11 16:16:07 -03:00
parent 55a2a9c545
commit 12e4cf58b6
12 changed files with 162 additions and 109 deletions
+5 -3
View File
@@ -125,11 +125,13 @@ class Notifications extends GObject.Object {
} }
public removeNotification(notif: (AstalNotifd.Notification|number)): void { 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) => this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) =>
item.id !== notifId); item.id !== notification.id);
notification.dismiss();
this.notify("notifications"); this.notify("notifications");
this.emit("notification-removed", notifId); this.emit("notification-removed", notification.id);
} }
connect(signal: string, callback: (...args: any[]) => void): number { connect(signal: string, callback: (...args: any[]) => void): number {
+41 -38
View File
@@ -1,11 +1,10 @@
import { execAsync, GLib, GObject, register, signal, writeFile } from "astal"; import { execAsync, GLib, GObject, property, register, signal } from "astal";
import { Subscribable } from "astal/binding"; import { Connectable } from "astal/binding";
import { Gdk } from "astal/gtk3"; import { Gdk } from "astal/gtk3";
import { getDateTime } from "./time"; import { getDateTime } from "./time";
import AstalWp from "gi://AstalWp";
@register({ GTypeName: "ScreenRecording" }) @register({ GTypeName: "ScreenRecording" })
class Recording extends GObject.Object implements Subscribable { class Recording extends GObject.Object implements Connectable {
private static instance: Recording; private static instance: Recording;
@@ -17,27 +16,44 @@ class Recording extends GObject.Object implements Subscribable {
declare outputChanged: (newPath: string) => void; declare outputChanged: (newPath: string) => void;
#recording: boolean = false; #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`; #path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`;
/** Default extension: mp4(h264) */ /** Default extension: mp4(h264) */
#extension: string = "mp4"; #extension: string = "mp4";
#recordAudio: boolean|AstalWp.Endpoint = false; // TODO #recordAudio: boolean = false;
#monitor: (number|null) = null;
#area: (Gdk.Rectangle|null) = null;
private notifySub() { @property(Boolean)
const subs = this.#subs; public get recording() { return this.#recording; }
for(const sub of subs) { private set recording(newValue: boolean) {
sub(this.recording); (!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; } @property(String)
private set recording(newValue: boolean) { this.#recording = newValue; }
public get path() { return this.#path; } 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 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() { constructor() {
super(); super();
@@ -50,39 +66,26 @@ class Recording extends GObject.Object implements Subscribable {
return this.instance; return this.instance;
} }
public get() { public startRecording(monitor?: number, area?: Gdk.Rectangle) {
return this.recording;
}
private emit(id: string, ...args: any[]) {
super.emit(id, ...args);
this.notifySub();
}
public startRecording(area?: Gdk.Rectangle) {
const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`; 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) ? `${Boolean(area) ?
`-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}`
: ""}`, : ""}`,
"-f", output ] `-f ${output}`
).then(() => { ]).then(() => {
this.emit("stopped", `${this.path}/${output}`); this.emit("stopped", `${this.path}/${output}`);
this.#recording = false;
this.notify("recording");
}); });
writeFile("", "");
this.emit("started");
this.notifySub();
} }
public stopRecording() { public stopRecording() {
} }
public subscribe(callback: (isRec: boolean) => void) {
this.#subs.add(callback);
return () => this.#subs.delete(callback);
}
} }
export { Recording }; export { Recording };
+29 -2
View File
@@ -57,7 +57,7 @@ window.ask-popup {
} }
.notification { .notification {
background: colors.$bg-primary; background: colors.$bg-translucent-secondary;
border-radius: 16px; border-radius: 16px;
& > .top { & > .top {
@@ -87,8 +87,9 @@ window.ask-popup {
} }
& .content { & .content {
padding: 4px; padding: 6px;
padding-top: 0; padding-top: 0;
& .image { & .image {
$size: 78px; $size: 78px;
min-width: $size; min-width: $size;
@@ -110,6 +111,32 @@ window.ask-popup {
font-weight: 400; 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 { tooltip {
+1
View File
@@ -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-tertiary: funs.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%));
$bg-light: wal.$foreground; $bg-light: wal.$foreground;
$bg-translucent: funs.toRGB(color.change($color: $bg-primary, $alpha: 75%)); $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-primary: wal.$foreground;
$fg-light: $bg-primary; $fg-light: $bg-primary;
$fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%)); $fg-disabled: funs.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%));
+5
View File
@@ -145,6 +145,11 @@
} }
} }
& .sub-header {
font-size: 18px;
font-weight: 500;
}
&.bluetooth { &.bluetooth {
.connections button { .connections button {
@include mixins.hover-shadow; @include mixins.hover-shadow;
+1 -1
View File
@@ -9,6 +9,6 @@
& > .notification { & > .notification {
margin: 6px; margin: 6px;
box-shadow: 0 0 4px .5px colors.$bg-translucent; box-shadow: 0 0 4px .5px colors.$bg-primary;
} }
} }
+2
View File
@@ -35,11 +35,13 @@
&:first-child { &:first-child {
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
margin-left: 0;
} }
&:last-child { &:last-child {
border-top-right-radius: 10px; border-top-right-radius: 10px;
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
margin-right: 0;
} }
} }
} }
+16
View File
@@ -101,6 +101,22 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
] ]
} as Widget.BoxProps) } 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)
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
+1 -1
View File
@@ -74,8 +74,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
widthRequest: props?.widthRequest, widthRequest: props?.widthRequest,
heightRequest: props?.heightRequest, heightRequest: props?.heightRequest,
hexpand: props?.hexpand || false, hexpand: props?.hexpand || false,
visible: true,
vexpand: props?.vexpand || false, vexpand: props?.vexpand || false,
visible: true,
css: `.popup { css: `.popup {
margin-top: ${props.marginTop || 0}px; margin-top: ${props.marginTop || 0}px;
margin-bottom: ${props.marginBottom || 0}px; margin-bottom: ${props.marginBottom || 0}px;
+4 -60
View File
@@ -2,6 +2,7 @@ import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import { Notifications } from "../../scripts/notifications"; import { Notifications } from "../../scripts/notifications";
import { NotificationWidget } from "../Notification";
export const NotifHistory: Gtk.Widget = new Widget.Scrollable({ export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
hscroll: Gtk.PolicyType.NEVER, hscroll: Gtk.PolicyType.NEVER,
@@ -10,65 +11,8 @@ export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
child: new Widget.Box({ child: new Widget.Box({
className: "notifications", className: "notifications",
children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) => children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) =>
history.map((notification: AstalNotifd.Notification) => history.map((notification: AstalNotifd.Notification) => NotificationWidget(notification,
new Widget.Box({ () => Notifications.getDefault().removeHistory(notification.id))
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)
)
)
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.ScrollableProps) } as Widget.ScrollableProps)
+54 -1
View File
@@ -1,4 +1,4 @@
import { Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./Page"; import { Page } from "./Page";
import AstalNetwork from "gi://AstalNetwork"; import AstalNetwork from "gi://AstalNetwork";
import { bind } from "astal"; import { bind } from "astal";
@@ -18,6 +18,59 @@ export const PageNetwork = new Page({
} as Widget.ButtonProps) } as Widget.ButtonProps)
], ],
pageChild: () => new Widget.Box({ 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) } as Widget.BoxProps)
}); });
+2 -2
View File
@@ -47,7 +47,7 @@ export namespace Runner {
width?: number; width?: number;
height?: number; height?: number;
entryPlaceHolder?: string; entryPlaceHolder?: string;
resultsPlaceholder?: () => Array<Gtk.Widget>; resultsPlaceholder?: () => Array<ResultWidget>;
}; };
export const prefixes = new Map<string, (entry: string) => (ResultWidget|Array<ResultWidget>|null)>([ export const prefixes = new Map<string, (entry: string) => (ResultWidget|Array<ResultWidget>|null)>([
@@ -58,7 +58,7 @@ export namespace Runner {
export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) { export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) {
let subs: Array<() => void> = []; let subs: Array<() => void> = [];
const entryText: Variable<string> = new Variable<string>(""); const entryText: Variable<string> = new Variable<string>("");
let results: (Array<ResultWidget>|null) = null; let results: (Array<ResultWidget>|null) = props?.resultsPlaceholder ? props.resultsPlaceholder() : null;
let selectedResultIndex = 0; let selectedResultIndex = 0;
const searchEntry = new Widget.Entry({ const searchEntry = new Widget.Entry({