✨ feat(modules/notifications, osd): pause notification timeout on holdNotification
osd now supports brightness controlsgit add .
This commit is contained in:
@@ -12,29 +12,23 @@
|
|||||||
-gtk-icon-size: 24px;
|
-gtk-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume {
|
.level {
|
||||||
margin-top: -6px;
|
margin-top: -6px;
|
||||||
|
|
||||||
.device {
|
.text {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
levelbar trough block {
|
levelbar trough block {
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
background: colors.$bg-primary;
|
background: colors.$bg-primary;
|
||||||
|
|
||||||
&.filled {
|
&.filled {
|
||||||
min-height: 8px;
|
min-height: 10px;
|
||||||
background: colors.$bg-secondary;
|
background: colors.$bg-secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-6
@@ -22,15 +22,15 @@ import { Clipboard } from "./modules/clipboard";
|
|||||||
import { Config } from "./modules/config";
|
import { Config } from "./modules/config";
|
||||||
import { Gdk, Gtk } from "ags/gtk4";
|
import { Gdk, Gtk } from "ags/gtk4";
|
||||||
import { createRoot, getScope } from "ags";
|
import { createRoot, getScope } from "ags";
|
||||||
import { triggerOSD } from "./window/OSD";
|
import { OSDModes, triggerOSD } from "./window/OSD";
|
||||||
import { programArgs, programInvocationName } from "system";
|
import { programArgs, programInvocationName } from "system";
|
||||||
import { setConsoleLogDomain } from "console";
|
import { setConsoleLogDomain } from "console";
|
||||||
import { initPlayer } from "./modules/media";
|
import { initPlayer } from "./modules/media";
|
||||||
import { encoder } from "./modules/utils";
|
import { encoder } from "./modules/utils";
|
||||||
import { exec } from "ags/process";
|
import { exec } from "ags/process";
|
||||||
|
import { Backlights } from "./modules/backlight";
|
||||||
import GObject, { register } from "ags/gobject";
|
import GObject, { register } from "ags/gobject";
|
||||||
|
|
||||||
import AstalNotifd from "gi://AstalNotifd";
|
|
||||||
import GLib from "gi://GLib?version=2.0";
|
import GLib from "gi://GLib?version=2.0";
|
||||||
import Gio from "gi://Gio?version=2.0";
|
import Gio from "gi://Gio?version=2.0";
|
||||||
import Adw from "gi://Adw?version=1";
|
import Adw from "gi://Adw?version=1";
|
||||||
@@ -283,15 +283,38 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re
|
|||||||
|
|
||||||
this.#connections.set(Wireplumber.getDefault(),
|
this.#connections.set(Wireplumber.getDefault(),
|
||||||
Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () =>
|
Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () =>
|
||||||
triggerOSD())
|
!Windows.getDefault().isOpen("control-center") &&
|
||||||
|
triggerOSD(OSDModes.SINK)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// dinamically connect to default backlight (if there's any)
|
||||||
|
let lastDefaultBk: Backlights.Backlight|null = null;
|
||||||
|
this.#connections.set(Backlights.getDefault(),
|
||||||
|
Backlights.getDefault().connect("notify::default", (_, defaultBk: Backlights.Backlight|null) => {
|
||||||
|
if(!lastDefaultBk) return;
|
||||||
|
|
||||||
|
if(this.#connections.has(lastDefaultBk))
|
||||||
|
lastDefaultBk.disconnect((this.#connections.get(lastDefaultBk) as number));
|
||||||
|
|
||||||
|
lastDefaultBk = null;
|
||||||
|
if(!defaultBk) return;
|
||||||
|
|
||||||
|
lastDefaultBk = defaultBk;
|
||||||
|
|
||||||
|
this.#connections.set(defaultBk, defaultBk.connect("brightness-changed", () =>
|
||||||
|
!Windows.getDefault().isOpen("control-center") &&
|
||||||
|
triggerOSD(OSDModes.BRIGHTNESS)
|
||||||
|
));
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#connections.set(Notifications.getDefault(), [
|
this.#connections.set(Notifications.getDefault(), [
|
||||||
Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => {
|
Notifications.getDefault().connect("notification-added", () => {
|
||||||
Windows.getDefault().open("floating-notifications");
|
Windows.getDefault().open("floating-notifications");
|
||||||
}),
|
}),
|
||||||
Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => {
|
Notifications.getDefault().connect("notification-removed", (self) => {
|
||||||
_.notifications.length === 0 && Windows.getDefault().close("floating-notifications");
|
self.notifications.length === 0 && Windows.getDefault().close("floating-notifications");
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -1,14 +1,17 @@
|
|||||||
import { execAsync, Gio, GLib, register } from "astal";
|
import { execAsync } from "ags/process";
|
||||||
import Polkit from "gi://Polkit";
|
import { register } from "ags/gobject";
|
||||||
import PolkitAgent from "gi://PolkitAgent";
|
|
||||||
import { EntryPopup, EntryPopupProps } from "../widget/EntryPopup";
|
import { EntryPopup, EntryPopupProps } from "../widget/EntryPopup";
|
||||||
import AstalAuth from "gi://AstalAuth";
|
|
||||||
import { AskPopup, AskPopupProps } from "../widget/AskPopup";
|
import { AskPopup, AskPopupProps } from "../widget/AskPopup";
|
||||||
|
|
||||||
export { Auth };
|
import AstalAuth from "gi://AstalAuth";
|
||||||
|
import Polkit from "gi://Polkit";
|
||||||
|
import PolkitAgent from "gi://PolkitAgent";
|
||||||
|
import Gio from "gi://Gio?version=2.0";
|
||||||
|
import GLib from "gi://GLib?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
@register({ GTypeName: "AuthAgent" })
|
@register({ GTypeName: "AuthAgent" })
|
||||||
class Auth extends PolkitAgent.Listener {
|
export class Auth extends PolkitAgent.Listener {
|
||||||
private static instance: Auth;
|
private static instance: Auth;
|
||||||
#subject: Polkit.Subject;
|
#subject: Polkit.Subject;
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,13 @@ export namespace Backlights {
|
|||||||
): void {
|
): void {
|
||||||
super.emit(signal, ...args);
|
super.emit(signal, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public connect<Signal extends keyof typeof this.$signals>(
|
||||||
|
signal: Signal,
|
||||||
|
callback: (self: typeof this, ...args: Parameters<(typeof this.$signals)[Signal]>) => ReturnType<(typeof this.$signals)[Signal]>
|
||||||
|
): number {
|
||||||
|
return super.connect(signal, callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Backlights = _Backlights;
|
export const Backlights = _Backlights;
|
||||||
|
|||||||
+160
-112
@@ -1,17 +1,13 @@
|
|||||||
import { timeout } from "ags/time";
|
|
||||||
import { execAsync } from "ags/process";
|
import { execAsync } from "ags/process";
|
||||||
import { readFile } from "ags/file";
|
|
||||||
import { generalConfig } from "../app";
|
import { generalConfig } from "../app";
|
||||||
import { onCleanup } from "ags";
|
import { onCleanup } from "ags";
|
||||||
import GObject, { getter, property, register, signal } from "ags/gobject";
|
import GObject, { getter, ParamSpec, property, register, signal } from "ags/gobject";
|
||||||
|
|
||||||
import AstalNotifd from "gi://AstalNotifd";
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
import AstalIO from "gi://AstalIO";
|
|
||||||
import Gio from "gi://Gio?version=2.0";
|
|
||||||
import GLib from "gi://GLib?version=2.0";
|
import GLib from "gi://GLib?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
export interface HistoryNotification {
|
export type HistoryNotification = {
|
||||||
id: number;
|
id: number;
|
||||||
appName: string;
|
appName: string;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -22,28 +18,100 @@ export interface HistoryNotification {
|
|||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotificationTimeout {
|
||||||
|
#source?: GLib.Source;
|
||||||
|
#args?: Array<any>;
|
||||||
|
#millis: number;
|
||||||
|
#lastRemained!: number;
|
||||||
|
|
||||||
|
readonly callback: () => void;
|
||||||
|
get millis(): number { return this.#millis; }
|
||||||
|
get remaining(): number { return this.source!.get_time() }
|
||||||
|
get lastRemained(): number { return this.#lastRemained; }
|
||||||
|
get running(): boolean { return Boolean(this.source?.is_destroyed()); }
|
||||||
|
get source(): GLib.Source|undefined { return this.#source; }
|
||||||
|
|
||||||
|
constructor(millis: number, callback: () => void, start: boolean = true, ...args: Array<any>) {
|
||||||
|
this.#millis = millis;
|
||||||
|
this.callback = callback;
|
||||||
|
this.#args = args;
|
||||||
|
|
||||||
|
if(!start) return;
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
// use lastRemained to calculate on what time the user hold the notification, so it
|
||||||
|
// can be released by the remaining time (works like a timeout "pause")
|
||||||
|
this.#lastRemained = Math.floor(Math.max(this.#source!.get_ready_time() - GLib.get_monotonic_time()) / 1000);
|
||||||
|
this.#source?.destroy();
|
||||||
|
this.#source?.unref();
|
||||||
|
this.#source = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(newMillis?: number): GLib.Source {
|
||||||
|
if(this.running)
|
||||||
|
throw new Error("Notifications: Can't start a new counter if it's already running!");
|
||||||
|
|
||||||
|
if(newMillis !== undefined)
|
||||||
|
this.#millis = newMillis;
|
||||||
|
|
||||||
|
this.#source = setTimeout(
|
||||||
|
this.callback,
|
||||||
|
this.#millis,
|
||||||
|
this.#args
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#lastRemained = Math.floor(Math.max(this.#source!.get_ready_time() - GLib.get_monotonic_time()) / 1000);
|
||||||
|
|
||||||
|
return this.#source;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@register({ GTypeName: "Notifications" })
|
@register({ GTypeName: "Notifications" })
|
||||||
class Notifications extends GObject.Object {
|
export class Notifications extends GObject.Object {
|
||||||
private static instance: (Notifications|null) = null;
|
private static instance: (Notifications|null) = null;
|
||||||
|
|
||||||
#notifications: Array<AstalNotifd.Notification> = [];
|
declare $signals: GObject.Object.SignalSignatures & {
|
||||||
|
"history-added": (notification: HistoryNotification) => void;
|
||||||
|
"history-removed": (notificationId: number) => void;
|
||||||
|
"history-cleared": () => void;
|
||||||
|
"notification-added": (notification: AstalNotifd.Notification) => void;
|
||||||
|
"notification-removed": (notificationId: number) => void;
|
||||||
|
"notification-replaced": (notificationId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
#notifications = new Map<number, [AstalNotifd.Notification, NotificationTimeout]>();
|
||||||
#history: Array<HistoryNotification> = [];
|
#history: Array<HistoryNotification> = [];
|
||||||
#notificationsOnHold: Set<number> = new Set<number>();
|
|
||||||
#connections: Array<number> = [];
|
#connections: Array<number> = [];
|
||||||
|
|
||||||
@getter(Array<AstalNotifd.Notification>)
|
@getter(Array<AstalNotifd.Notification>)
|
||||||
public get notifications() { return this.#notifications };
|
public get notifications() {
|
||||||
|
return [...this.#notifications.values()].map(([n]) => n);
|
||||||
|
};
|
||||||
|
|
||||||
@getter(Array<HistoryNotification>)
|
@getter(Array<HistoryNotification>)
|
||||||
public get history() { return this.#history };
|
public get history() { return this.#history };
|
||||||
|
|
||||||
|
@getter(Array<AstalNotifd.Notification>)
|
||||||
|
public get notificationsOnHold() {
|
||||||
|
return [...this.#notifications.values()].filter(([_, s]) =>
|
||||||
|
typeof s === "undefined"
|
||||||
|
).map(([n]) => n);
|
||||||
|
}
|
||||||
|
|
||||||
@property(Number)
|
@property(Number)
|
||||||
public historyLimit: number = 10;
|
public historyLimit: number = 10;
|
||||||
|
|
||||||
|
/** skip notifications directly to notification history */
|
||||||
|
@property(Boolean)
|
||||||
|
public ignoreNotifications: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
@signal(AstalNotifd.Notification) notificationAdded(_notification: AstalNotifd.Notification) {};
|
@signal(AstalNotifd.Notification) notificationAdded(_notification: AstalNotifd.Notification) {};
|
||||||
@signal(Number) notificationRemoved(_id: number) {};
|
@signal(Number) notificationRemoved(_id: number) {};
|
||||||
@signal(Object) historyAdded(_notification: Object) {};
|
@signal(Object as unknown as ParamSpec<HistoryNotification>) historyAdded(_notification: Object) {};
|
||||||
|
@signal() historyCleared() {};
|
||||||
@signal(Number) historyRemoved(_id: number) {};
|
@signal(Number) historyRemoved(_id: number) {};
|
||||||
@signal(Number) notificationReplaced(_id: number) {};
|
@signal(Number) notificationReplaced(_id: number) {};
|
||||||
|
|
||||||
@@ -53,43 +121,13 @@ class Notifications extends GObject.Object {
|
|||||||
this.#connections.push(
|
this.#connections.push(
|
||||||
AstalNotifd.get_default().connect("notified", (notifd, id) => {
|
AstalNotifd.get_default().connect("notified", (notifd, id) => {
|
||||||
const notification = notifd.get_notification(id);
|
const notification = notifd.get_notification(id);
|
||||||
const notifTimeout = generalConfig.getProperty(
|
|
||||||
`notifications.timeout_${this.getUrgencyString(notification.urgency).toLowerCase()}`,
|
|
||||||
"number") as number;
|
|
||||||
|
|
||||||
if(this.getNotifd().dontDisturb) {
|
if(this.getNotifd().dontDisturb || this.ignoreNotifications) {
|
||||||
this.addHistory(notification, () => notification.dismiss());
|
this.addHistory(notification, () => notification.dismiss());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addNotification(notification, () => {
|
this.addNotification(notification, this.getNotificationTimeout(notification) > 0);
|
||||||
if(notification.urgency !== AstalNotifd.Urgency.CRITICAL ||
|
|
||||||
(notification.urgency === AstalNotifd.Urgency.CRITICAL &&
|
|
||||||
notifTimeout > 0)) {
|
|
||||||
|
|
||||||
let notifTimer: (AstalIO.Time|undefined) = undefined;
|
|
||||||
let replacedConnectionId: number;
|
|
||||||
|
|
||||||
const removeFun = () => { // Funny name haha lmao remove fun :skull:
|
|
||||||
notifTimer = undefined;
|
|
||||||
if(this.#notificationsOnHold.has(notification.id)) return;
|
|
||||||
|
|
||||||
this.addHistory(notification, () => {
|
|
||||||
replacedConnectionId && this.disconnect(replacedConnectionId);
|
|
||||||
this.removeNotification(id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
notifTimer = timeout(notifTimeout, removeFun);
|
|
||||||
|
|
||||||
replacedConnectionId = this.connect("notification-replaced", (_, id: number) => {
|
|
||||||
if(notification.id !== id) return;
|
|
||||||
|
|
||||||
notifTimer?.cancel();
|
|
||||||
notifTimer = timeout(notifTimeout, removeFun);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => {
|
AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => {
|
||||||
@@ -98,8 +136,6 @@ class Notifications extends GObject.Object {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.retrieveHistoryFromFile();
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
this.#connections.map(id =>
|
this.#connections.map(id =>
|
||||||
AstalNotifd.get_default().disconnect(id));
|
AstalNotifd.get_default().disconnect(id));
|
||||||
@@ -113,42 +149,6 @@ class Notifications extends GObject.Object {
|
|||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private retrieveHistoryFromFile(): void {
|
|
||||||
const historyFile = Gio.File.new_for_path(`${GLib.get_user_state_dir()}/astal/notifd/notifications.json`);
|
|
||||||
if(!historyFile.query_exists(null)) return;
|
|
||||||
|
|
||||||
let content: string;
|
|
||||||
console.log("Notifications: History file found! Trying to retrieve history from JSON");
|
|
||||||
|
|
||||||
try {
|
|
||||||
content = readFile(historyFile.get_path()!);
|
|
||||||
} catch(e: any) {
|
|
||||||
console.error(`Notifications: An error occurred while trying to read the history file. Stderr:\n${
|
|
||||||
(e as Error).message}\n${(e as Error).stack}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const historyJSON = JSON.parse(content);
|
|
||||||
|
|
||||||
(historyJSON["notifications"] as Array<AstalNotifd.Notification>).reverse()
|
|
||||||
.forEach(n => this.addHistory(n));
|
|
||||||
} catch(e: any) {
|
|
||||||
if(e instanceof SyntaxError) {
|
|
||||||
console.error(`Notifications: Couldn't parse history JSON because of a SyntaxError:\n${e.message
|
|
||||||
}\n${e.stack}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Notifications: An error occurred while parsing the history JSON file. Stderr:\n${
|
|
||||||
e.message}\n${e.stack}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendNotification(props: {
|
public async sendNotification(props: {
|
||||||
urgency?: AstalNotifd.Urgency;
|
urgency?: AstalNotifd.Urgency;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
@@ -236,7 +236,7 @@ class Notifications extends GObject.Object {
|
|||||||
|
|
||||||
this.notify("history");
|
this.notify("history");
|
||||||
this.emit("history-added", this.#history[0]);
|
this.emit("history-added", this.#history[0]);
|
||||||
onAdded && onAdded(notif);
|
onAdded?.(notif);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clearHistory(): Promise<void> {
|
public async clearHistory(): Promise<void> {
|
||||||
@@ -245,6 +245,7 @@ class Notifications extends GObject.Object {
|
|||||||
this.emit("history-removed", notif.id);
|
this.emit("history-removed", notif.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.emit("history-cleared");
|
||||||
this.notify("history");
|
this.notify("history");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,47 +258,82 @@ class Notifications extends GObject.Object {
|
|||||||
this.emit("history-removed", notifId);
|
this.emit("history-removed", notifId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addNotification(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void {
|
private addNotification(
|
||||||
for(let i = 0; i < this.#notifications.length; i++) {
|
notif: AstalNotifd.Notification,
|
||||||
const item = this.#notifications[i];
|
removeOnTimeout: boolean = true,
|
||||||
|
onTimeoutEnd?: () => void
|
||||||
|
): void {
|
||||||
|
|
||||||
if(item.id !== notif.id) continue;
|
const replaced = this.#notifications.has(notif.id);
|
||||||
|
const notifTimeout = this.getNotificationTimeout(notif);
|
||||||
this.#notifications.splice(i, 1);
|
const onEnd = () => {
|
||||||
this.emit("notification-replaced", item.id);
|
removeOnTimeout && this.removeNotification(notif);
|
||||||
break;
|
onTimeoutEnd?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#notifications.unshift(notif);
|
// destroy timer of replaced notification(if there's any)
|
||||||
|
if(replaced) {
|
||||||
|
const data = this.#notifications.get(notif.id)!;
|
||||||
|
(data?.[1] instanceof NotificationTimeout) &&
|
||||||
|
data[1].cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#notifications.set(notif.id, [
|
||||||
|
notif,
|
||||||
|
new NotificationTimeout(notifTimeout, onEnd, notifTimeout > 0)
|
||||||
|
]);
|
||||||
|
|
||||||
|
replaced && this.emit("notification-replaced", notif.id);
|
||||||
|
|
||||||
this.notify("notifications");
|
this.notify("notifications");
|
||||||
this.emit("notification-added", notif);
|
this.emit("notification-added", notif);
|
||||||
onAdded?.(notif);
|
|
||||||
|
if(notifTimeout <= 0) onEnd?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeNotification(notif: (AstalNotifd.Notification|number)): void {
|
public getNotificationTimeout(notif: AstalNotifd.Notification): number {
|
||||||
const notificationId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif;
|
return generalConfig.getProperty(
|
||||||
this.#notificationsOnHold.delete(notificationId);
|
`notifications.timeout_${this.getUrgencyString(notif.urgency)}`,
|
||||||
|
"number"
|
||||||
this.#notifications = this.#notifications.filter((item) =>
|
);
|
||||||
item.id !== notificationId);
|
|
||||||
|
|
||||||
AstalNotifd.get_default().get_notification(notificationId)?.dismiss();
|
|
||||||
this.notify("notifications");
|
|
||||||
this.emit("notification-removed", notificationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNotificationById(id: number): AstalNotifd.Notification|undefined {
|
public removeNotification(notif: (AstalNotifd.Notification|number), addToHistory: boolean = true): void {
|
||||||
return this.#notifications.filter(notif => notif.id === id)?.[0];
|
notif = typeof notif === "number" ?
|
||||||
}
|
this.#notifications.get(notif)?.[0]!
|
||||||
|
|
||||||
public holdNotification(notif: (AstalNotifd.Notification|number)): void {
|
|
||||||
notif = (typeof notif === "number") ?
|
|
||||||
this.getNotificationById(notif)!
|
|
||||||
: notif;
|
: notif;
|
||||||
|
|
||||||
if(!notif) return;
|
if(!notif) return;
|
||||||
|
|
||||||
this.#notificationsOnHold.add(notif.id);
|
const timeout = this.#notifications.get(notif.id)![1];
|
||||||
|
timeout.running && timeout.cancel();
|
||||||
|
|
||||||
|
this.#notifications.delete(notif.id);
|
||||||
|
addToHistory && this.addHistory(notif);
|
||||||
|
|
||||||
|
notif.dismiss();
|
||||||
|
this.notify("notifications");
|
||||||
|
this.emit("notification-removed", notif.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public holdNotification(notif: AstalNotifd.Notification|number): void {
|
||||||
|
const id = typeof notif === "number" ? notif : notif.id;
|
||||||
|
const data = this.#notifications.get(id);
|
||||||
|
|
||||||
|
if(!data) return;
|
||||||
|
|
||||||
|
data[1].cancel();
|
||||||
|
this.notify("notifications-on-hold");
|
||||||
|
}
|
||||||
|
|
||||||
|
public releaseNotification(notif: AstalNotifd.Notification|number): void {
|
||||||
|
const id = typeof notif === "number" ? notif : notif.id;
|
||||||
|
const data = this.#notifications.get(id);
|
||||||
|
|
||||||
|
if(!data) return;
|
||||||
|
data[1].start(data[1].lastRemained);
|
||||||
|
|
||||||
|
this.notify("notifications-on-hold");
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleDoNotDisturb(value?: boolean): boolean {
|
public toggleDoNotDisturb(value?: boolean): boolean {
|
||||||
@@ -308,6 +344,18 @@ class Notifications extends GObject.Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); }
|
public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); }
|
||||||
}
|
|
||||||
|
|
||||||
export { Notifications };
|
public emit<Signal extends keyof typeof this.$signals>(
|
||||||
|
signal: Signal, ...args: Parameters<(typeof this.$signals)[Signal]>
|
||||||
|
): void {
|
||||||
|
super.emit(signal, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect<Signal extends keyof typeof this.$signals>(
|
||||||
|
signal: Signal,
|
||||||
|
callback: (self: typeof this, ...params: Parameters<(typeof this.$signals)[Signal]>) =>
|
||||||
|
ReturnType<(typeof this.$signals)[Signal]>
|
||||||
|
): number {
|
||||||
|
return super.connect(signal, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getSymbolicIcon } from "./apps";
|
|||||||
|
|
||||||
import GLib from "gi://GLib?version=2.0";
|
import GLib from "gi://GLib?version=2.0";
|
||||||
import Gio from "gi://Gio?version=2.0";
|
import Gio from "gi://Gio?version=2.0";
|
||||||
|
import GObject from "gi://GObject?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
/** gnim doesn't export this, so we need to do it again */
|
/** gnim doesn't export this, so we need to do it again */
|
||||||
@@ -214,3 +215,29 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu
|
|||||||
|
|
||||||
return slider;
|
return slider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** initialize and sub class properties with accessors */
|
||||||
|
export function construct(klass: object, props: Record<any, any|Accessor<any>>): Array<() => void> {
|
||||||
|
|
||||||
|
const subs: Array<() => void> = [];
|
||||||
|
const isGObject = klass instanceof GObject.Object;
|
||||||
|
|
||||||
|
Object.keys(props).forEach(k => {
|
||||||
|
const v = props[k as keyof typeof props];
|
||||||
|
|
||||||
|
if(v === undefined) return;
|
||||||
|
if(v instanceof Accessor) {
|
||||||
|
subs.push(v.subscribe(() => {
|
||||||
|
klass[k as keyof typeof klass] = v.get() as never;
|
||||||
|
if(isGObject) klass.notify(k);
|
||||||
|
}));
|
||||||
|
|
||||||
|
klass[k as keyof typeof klass] = v.get() as never;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
klass[k as keyof typeof klass] = v as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
return subs;
|
||||||
|
}
|
||||||
|
|||||||
+17
-30
@@ -9,7 +9,6 @@ import GObject from "ags/gobject";
|
|||||||
import AstalNotifd from "gi://AstalNotifd";
|
import AstalNotifd from "gi://AstalNotifd";
|
||||||
import Pango from "gi://Pango?version=1.0";
|
import Pango from "gi://Pango?version=1.0";
|
||||||
import GLib from "gi://GLib?version=2.0";
|
import GLib from "gi://GLib?version=2.0";
|
||||||
import { timeout } from "ags/time";
|
|
||||||
|
|
||||||
|
|
||||||
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
|
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
|
||||||
@@ -42,36 +41,24 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
|
|||||||
|
|
||||||
const conns: Map<GObject.Object, Array<number>> = new Map();
|
const conns: Map<GObject.Object, Array<number>> = new Map();
|
||||||
|
|
||||||
onCleanup(() =>
|
onCleanup(() => conns.forEach((ids, obj) =>
|
||||||
conns.forEach((ids, obj) => ids.forEach(id => obj.disconnect(id))));
|
ids.forEach(id => obj.disconnect(id))
|
||||||
|
));
|
||||||
|
|
||||||
return <Gtk.Box hexpand class={`notification ${
|
return <Gtk.Box hexpand class={`notification ${
|
||||||
Notifications.getDefault().getUrgencyString(notification.urgency)
|
Notifications.getDefault().getUrgencyString(notification.urgency)
|
||||||
}`} orientation={Gtk.Orientation.VERTICAL} spacing={5}
|
}`} orientation={Gtk.Orientation.VERTICAL} spacing={5}>
|
||||||
$={(self) => {
|
|
||||||
const eventControllerMotion = Gtk.EventControllerMotion.new(),
|
|
||||||
gestureClick = Gtk.GestureClick.new();
|
|
||||||
|
|
||||||
self.add_controller(eventControllerMotion);
|
<Gtk.EventControllerMotion onEnter={() => holdOnHover &&
|
||||||
self.add_controller(gestureClick);
|
Notifications.getDefault().holdNotification(notification.id)
|
||||||
|
} onLeave={() => holdOnHover &&
|
||||||
conns.set(eventControllerMotion, [
|
Notifications.getDefault().releaseNotification(notification.id)
|
||||||
eventControllerMotion.connect("enter", () =>
|
}
|
||||||
holdOnHover && Notifications.getDefault().holdNotification(notification.id)),
|
/>
|
||||||
eventControllerMotion.connect("leave", () =>
|
<Gtk.GestureClick onReleased={(gesture) =>
|
||||||
holdOnHover && notification && timeout(600, () =>
|
|
||||||
Notifications.getDefault().removeNotification(notification.id)
|
|
||||||
))
|
|
||||||
]);
|
|
||||||
|
|
||||||
conns.set(gestureClick, [
|
|
||||||
gestureClick.connect("released", (gesture) => {
|
|
||||||
gesture.get_current_button() === Gdk.BUTTON_PRIMARY &&
|
gesture.get_current_button() === Gdk.BUTTON_PRIMARY &&
|
||||||
actionClicked?.(notification);
|
actionClicked?.(notification)
|
||||||
})
|
} />
|
||||||
]);
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<Gtk.Box class={"top"} hexpand>
|
<Gtk.Box class={"top"} hexpand>
|
||||||
<Gtk.Image class="app-icon" $={(self) => {
|
<Gtk.Image class="app-icon" $={(self) => {
|
||||||
const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ??
|
const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ??
|
||||||
@@ -84,7 +71,7 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
|
|||||||
|
|
||||||
self.set_visible(false);
|
self.set_visible(false);
|
||||||
}} />
|
}} />
|
||||||
<Gtk.Label class={"app-name"} halign={Gtk.Align.START} hexpand={true}
|
<Gtk.Label class={"app-name"} halign={Gtk.Align.START} hexpand
|
||||||
label={notification.appName || "Application"} />
|
label={notification.appName || "Application"} />
|
||||||
|
|
||||||
<Gtk.Label class={"time"} visible={showTime} xalign={1}
|
<Gtk.Label class={"time"} visible={showTime} xalign={1}
|
||||||
@@ -101,14 +88,14 @@ export function NotificationWidget({ notification, actionClicked, holdOnHover, s
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL}
|
<Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL}
|
||||||
vexpand={true}>
|
vexpand>
|
||||||
|
|
||||||
<Gtk.Label class={"summary"} useMarkup={true} hexpand xalign={0}
|
<Gtk.Label class={"summary"} useMarkup hexpand xalign={0}
|
||||||
vexpand={false} ellipsize={Pango.EllipsizeMode.END} label={
|
vexpand={false} ellipsize={Pango.EllipsizeMode.END} label={
|
||||||
escapeUnintendedMarkup(notification.summary)}
|
escapeUnintendedMarkup(notification.summary)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Gtk.Label class={"body"} useMarkup={true} xalign={0} wrap={true} hexpand
|
<Gtk.Label class={"body"} useMarkup xalign={0} wrap hexpand
|
||||||
vexpand wrapMode={Pango.WrapMode.WORD_CHAR} valign={Gtk.Align.START} label={
|
vexpand wrapMode={Pango.WrapMode.WORD_CHAR} valign={Gtk.Align.START} label={
|
||||||
escapeUnintendedMarkup(notification.body)}
|
escapeUnintendedMarkup(notification.body)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+74
-32
@@ -1,64 +1,106 @@
|
|||||||
import { Astal, Gtk } from "ags/gtk4";
|
import { Astal, Gtk } from "ags/gtk4";
|
||||||
import { createBinding, createState } from "ags";
|
import { Accessor, createBinding, createState, With } from "ags";
|
||||||
import { Wireplumber } from "../modules/volume";
|
import { Wireplumber } from "../modules/volume";
|
||||||
import { Windows } from "../windows";
|
import { Windows } from "../windows";
|
||||||
import { Time, timeout } from "ags/time";
|
import { Backlights } from "../modules/backlight";
|
||||||
|
import { construct, variableToBoolean } from "../modules/utils";
|
||||||
|
|
||||||
|
import GObject, { ParamSpec, property, register } from "ags/gobject";
|
||||||
import Pango from "gi://Pango?version=1.0";
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
import GLib from "gi://GLib?version=2.0";
|
||||||
|
|
||||||
|
|
||||||
export enum OSDModes {
|
@register({ GTypeName: "OSDMode" })
|
||||||
SINK,
|
export class OSDMode extends GObject.Object {
|
||||||
BRIGHTNESS,
|
readonly #subs: Array<() => void> = [];
|
||||||
NONE
|
@property(String)
|
||||||
|
icon: string = "image-missing";
|
||||||
|
@property(Number)
|
||||||
|
value: number = 0;
|
||||||
|
@property(Number)
|
||||||
|
max: number = 100;
|
||||||
|
@property(String as unknown as ParamSpec<string|null>)
|
||||||
|
text: string|null = null;
|
||||||
|
|
||||||
|
constructor(props: {
|
||||||
|
icon: string | Accessor<string>;
|
||||||
|
value: number | Accessor<number>;
|
||||||
|
max?: number | Accessor<number>;
|
||||||
|
text?: string | Accessor<string>;
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
this.#subs = construct(this, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_dispose(): void {
|
||||||
|
this.#subs.forEach(s => s());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [osdMode, setOSDMode] = createState(OSDModes.NONE);
|
export const OSDModes: Record<string, OSDMode> = {
|
||||||
let osdTimer: (Time|undefined), osdTimeout = 3500;
|
SINK: new OSDMode({
|
||||||
|
icon: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "volumeIcon"),
|
||||||
|
value: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "volume"),
|
||||||
|
text: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "description"),
|
||||||
|
max: Wireplumber.getDefault().getMaxSinkVolume() / 100
|
||||||
|
}),
|
||||||
|
BRIGHTNESS: Backlights.getDefault().available ? new OSDMode({
|
||||||
|
icon: "display-brightness-symbolic",
|
||||||
|
value: createBinding(Backlights.getDefault().default, "brightness"),
|
||||||
|
max: createBinding(Backlights.getDefault().default, "maxBrightness"),
|
||||||
|
text: createBinding(Backlights.getDefault().default, "name")
|
||||||
|
})
|
||||||
|
: new OSDMode({
|
||||||
|
icon: "display-brightness-symbolic",
|
||||||
|
value: 100,
|
||||||
|
max: 100,
|
||||||
|
text: "No Backlight found"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const OSD = (mon: number) => {
|
const [osdMode, setOSDMode] = createState(OSDModes.SINK);
|
||||||
if(osdMode.get() === OSDModes.NONE)
|
let osdTimer: (GLib.Source|undefined), osdTimeout = 3500;
|
||||||
setOSDMode(OSDModes.SINK);
|
|
||||||
|
|
||||||
return <Astal.Window namespace={"osd"} class={"osd-window"} layer={Astal.Layer.OVERLAY}
|
export const OSD = (mon: number) =>
|
||||||
|
<Astal.Window namespace={"osd"} class={"osd-window"} layer={Astal.Layer.OVERLAY}
|
||||||
anchor={Astal.WindowAnchor.BOTTOM} focusable={false} marginBottom={80} monitor={mon}>
|
anchor={Astal.WindowAnchor.BOTTOM} focusable={false} marginBottom={80} monitor={mon}>
|
||||||
|
|
||||||
<Gtk.Box class={"osd"}>
|
<Gtk.Box class={"osd"}>
|
||||||
<Gtk.Image class={"icon"} iconName={createBinding(Wireplumber.getDefault().getDefaultSink(),
|
<With value={osdMode}>
|
||||||
"volumeIcon").as(icon => !Wireplumber.getDefault().isMutedSink() &&
|
{(mode: OSDMode) => <Gtk.Box>
|
||||||
Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic")}
|
<Gtk.Image class={"icon"} iconName={
|
||||||
/>
|
createBinding(mode, "icon")
|
||||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"volume"} vexpand={true} hexpand={true}>
|
} />
|
||||||
<Gtk.Label class={"device"} label={createBinding(Wireplumber.getDefault().getDefaultSink(),
|
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"level"} vexpand hexpand>
|
||||||
"description").as(description => description ?? "Speaker")}
|
<Gtk.Label class={"text"} label={createBinding(mode, "text").as(t => t ?? "")}
|
||||||
ellipsize={Pango.EllipsizeMode.END}
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
visible={variableToBoolean(createBinding(mode, "text"))}
|
||||||
/>
|
/>
|
||||||
<Gtk.LevelBar class={"levelbar"} value={createBinding(
|
<Gtk.LevelBar value={createBinding(mode, "value")} hexpand
|
||||||
Wireplumber.getDefault().getDefaultSink(), "volume")}
|
maxValue={createBinding(mode, "max")}
|
||||||
maxValue={Wireplumber.getDefault().getMaxSinkVolume() / 100}
|
|
||||||
/>
|
/>
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
|
</Gtk.Box>}
|
||||||
|
</With>
|
||||||
</Gtk.Box>
|
</Gtk.Box>
|
||||||
</Astal.Window>
|
</Astal.Window>;
|
||||||
}
|
|
||||||
|
|
||||||
export function triggerOSD() {
|
|
||||||
if(Windows.getDefault().isOpen("control-center")) return;
|
|
||||||
|
|
||||||
|
export function triggerOSD(mode: OSDMode) {
|
||||||
|
setOSDMode(mode);
|
||||||
Windows.getDefault().open("osd");
|
Windows.getDefault().open("osd");
|
||||||
|
|
||||||
if(!osdTimer) {
|
if(!osdTimer) {
|
||||||
osdTimer = timeout(osdTimeout, () => {
|
osdTimer = setTimeout(() => {
|
||||||
osdTimer = undefined;
|
osdTimer = undefined;
|
||||||
Windows.getDefault().close("osd");
|
Windows.getDefault().close("osd");
|
||||||
});
|
}, osdTimeout);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
osdTimer.cancel();
|
osdTimer.destroy();
|
||||||
osdTimer = timeout(osdTimeout, () => {
|
osdTimer = setTimeout(() => {
|
||||||
Windows.getDefault().close("osd");
|
Windows.getDefault().close("osd");
|
||||||
osdTimer = undefined;
|
osdTimer = undefined;
|
||||||
});
|
}, osdTimeout);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user