From 7f3e66cc71db6c9b499d2edf1efc8367ee0bb987 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Wed, 24 Sep 2025 21:48:34 -0300 Subject: [PATCH] :sparkles: feat: a lot of new stuff! support for default bluetooth adapter, notification popup position in configuration, code improvements --- resources/styles/_center-window.scss | 6 +- src/app.ts | 8 +- src/modules/apps.ts | 4 +- src/modules/bluetooth.ts | 81 ++++++++ src/modules/recording.ts | 82 +++++--- src/modules/utils.ts | 48 ++++- src/widget/bar/Status.tsx | 28 +-- src/widget/control-center/pages/Bluetooth.tsx | 192 +++++++++++------- src/widget/control-center/tiles/Bluetooth.tsx | 14 +- src/window/FloatingNotifications.tsx | 80 ++++++-- 10 files changed, 383 insertions(+), 160 deletions(-) create mode 100644 src/modules/bluetooth.ts diff --git a/resources/styles/_center-window.scss b/resources/styles/_center-window.scss index 2363bd9..e688cb6 100644 --- a/resources/styles/_center-window.scss +++ b/resources/styles/_center-window.scss @@ -89,6 +89,7 @@ & calendar.view { $border-radius: 14px; + font-weight: 600; background: colors.$bg-primary; border-radius: $border-radius; @@ -104,7 +105,10 @@ margin: 4px; label.day-number { - min-height: 22px; + $size: 24px; + + min-height: $size; + min-width: $size; } } diff --git a/src/app.ts b/src/app.ts index ae77bcc..ad743d3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -342,7 +342,13 @@ const generalConfigDefaults = { notifications: { timeout_low: 4000, timeout_normal: 6000, - timeout_critical: 0 + timeout_critical: 0, + /** notification popup horizontal position. can be "left" or "right" + * @default "right" */ + position_h: "right", + /** vertical notification popup position. can be "top" or "bottom" + * @default "top" */ + position_v: "top" }, night_light: { diff --git a/src/modules/apps.ts b/src/modules/apps.ts index cac7e75..86b868e 100644 --- a/src/modules/apps.ts +++ b/src/modules/apps.ts @@ -28,10 +28,10 @@ export function getAstalApps(): AstalApps.Apps { /** handles running with uwsm if it's installed */ export function execApp(app: AstalApps.Application|string, dispatchExecArgs?: string) { const executable = (typeof app === "string") ? app - : app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, ""); + : app.executable.replace(/%[fFcuUik]/g, ""); AstalHyprland.get_default().dispatch("exec", - `${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm app -- " : ""}${executable}` + `${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm-app -- " : ""}${executable}` ); } diff --git a/src/modules/bluetooth.ts b/src/modules/bluetooth.ts new file mode 100644 index 0000000..881bfdd --- /dev/null +++ b/src/modules/bluetooth.ts @@ -0,0 +1,81 @@ +import { createRoot, getScope, Scope } from "ags"; +import GObject, { getter, gtype, register, setter } from "ags/gobject"; +import AstalBluetooth from "gi://AstalBluetooth"; + + +/** AstalBluetooth helper (implements the default adapter feature) */ +@register({ GTypeName: "Bluetooth" }) +export class Bluetooth extends GObject.Object { + private static instance: Bluetooth; + private astalBl = AstalBluetooth.get_default(); + + #connections: Map|number> = new Map(); + #adapter: AstalBluetooth.Adapter|null = this.astalBl.adapter ?? null; + #scope!: Scope; + #isAvailable: boolean = false; + + @getter(Boolean) + get isAvailable() { return this.#isAvailable; } + + @getter(gtype(AstalBluetooth.Adapter)) + get adapter() { return this.#adapter; } + + @setter(gtype(AstalBluetooth.Adapter)) + set adapter(newAdapter: AstalBluetooth.Adapter|null) { + this.#adapter = newAdapter; + this.notify("adapter"); + } + + constructor() { + super(); + + createRoot((_) => { + this.#scope = getScope(); + + this.#connections.set( + AstalBluetooth.get_default(), + AstalBluetooth.get_default().connect("adapter-added", (self, adapter) => { + if(self.adapters.length === 1) // adapter was just added + this.adapter = adapter; + }) + ); + + this.#connections.set( + AstalBluetooth.get_default(), + AstalBluetooth.get_default().connect("adapter-removed", (self, adapter) => { + if(self.adapters.length < 1) { + this.adapter = null; + this.#isAvailable = false; + this.notify("is-available"); + } + + if(this.#adapter?.address !== adapter.address) + return; + + // the removed adapter was the default + + if(self.adapters.length < 1) { + this.adapter = null; + this.#isAvailable = false; + this.notify("is-available"); + + return; + } + + this.#adapter = self.adapters[0]; + }) + ); + }); + } + + public static getDefault(): Bluetooth { + if(!this.instance) + this.instance = new Bluetooth(); + + return this.instance; + } + + vfunc_dispose(): void { + this.#scope.dispose(); + } +} diff --git a/src/modules/recording.ts b/src/modules/recording.ts index a265089..c978fc2 100644 --- a/src/modules/recording.ts +++ b/src/modules/recording.ts @@ -1,6 +1,7 @@ import { execAsync } from "ags/process"; import { getter, register, signal } from "ags/gobject"; import { Gdk } from "ags/gtk4"; +import { createRoot, getScope, Scope } from "ags"; import { makeDirectory } from "./utils"; import { Notifications } from "./notifications"; import { time } from "./utils"; @@ -10,10 +11,8 @@ import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; -export { Recording }; - @register({ GTypeName: "Recording" }) -class Recording extends GObject.Object { +export class Recording extends GObject.Object { private static instance: Recording; @signal() started() {}; @@ -21,6 +20,7 @@ class Recording extends GObject.Object { #recording: boolean = false; #path: string = "~/Recordings"; + #recordingScope?: Scope; /** Default extension: mp4(h264) */ #extension: string = "mp4"; @@ -64,7 +64,22 @@ class Recording extends GObject.Object { this.notify("extension"); } - /** Recording output file name. %NULL if screen is not being recorded */ + @getter(String) + public get recordingTime() { + if(!this.#recording || !this.#startedAt) + return "not recording"; + + const startedAtSeconds = time.get().to_unix() - Recording.getDefault().startedAt!; + if(startedAtSeconds <= 0) return "00:00"; + + const seconds = Math.floor(startedAtSeconds % 60); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + return `${hours > 0 ? `${hours < 10 ? '0' : ""}${hours}` : ""}${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`; + } + + /** Recording output file name. null if screen is not being recorded */ public get output() { return this.#output; } /** Currently unsupported property */ @@ -90,41 +105,51 @@ class Recording extends GObject.Object { } public startRecording(area?: Gdk.Rectangle) { - if(this.recording) + if(this.#recording) throw new Error("Screen Recording is already running!"); - this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`; - this.#recording = true; - this.notify("recording"); - this.emit("started"); - makeDirectory(this.path); + createRoot(() => { + this.#recordingScope = getScope(); - const cancellable = Gio.Cancellable.new(); - cancellable.cancel = () => {}; + this.#output = `${time.get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`; + this.#recording = true; + this.notify("recording"); + this.emit("started"); + makeDirectory(this.path); - const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`; + const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`; - this.#process = Gio.Subprocess.new([ - "wf-recorder", - ...(area ? [ `-g`, areaString ] : []), - "-f", - `${this.path}/${this.output!}` - ], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + this.#process = Gio.Subprocess.new([ + "wf-recorder", + ...(area ? [ `-g`, areaString ] : []), + "-f", + `${this.path}/${this.output!}` + ], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); - this.#process.wait_async(cancellable, () => { - this.stopRecording(); + this.#process.wait_async(null, () => { + this.stopRecording(); + }); + + this.#startedAt = time.get().to_unix(); + this.notify("started-at"); + + const timeSub = time.subscribe(() => { + this.notify("recording-time"); + }); + + this.#recordingScope.onCleanup(timeSub); }); - - this.#startedAt = time.get().to_unix(); } public stopRecording() { - if(!this.#process) return; + if(!this.#process || !this.#recording) return; !this.#process.get_if_exited() && execAsync([ "kill", "-s", "SIGTERM", this.#process.get_identifier()! ]); + this.#recordingScope?.dispose(); + const path = this.#path; const output = this.#output; @@ -138,13 +163,8 @@ class Recording extends GObject.Object { Notifications.getDefault().sendNotification({ actions: [ { - text: "View", - onAction: () => { - execAsync(["nautilus", "-s", output!, path]); - } - }, - { - text: "Open", + text: "View", // will be hidden(can be triggered by clicking in the notification) + id: "view", onAction: () => { execAsync(["xdg-open", `${path}/${output}`]); } diff --git a/src/modules/utils.ts b/src/modules/utils.ts index ec3cea0..aaf5d7f 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,6 +1,6 @@ import { createPoll } from "ags/time"; import { exec, execAsync } from "ags/process"; -import { Accessor, For, With } from "ags"; +import { Accessor, For, getScope, onCleanup, With } from "ags"; import { Astal, Gtk } from "ags/gtk4"; import { getSymbolicIcon } from "./apps"; @@ -217,7 +217,7 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu } /** initialize and sub class properties with accessors */ -export function construct(klass: object, props: Record>): Array<() => void> { +export function construct(klass: Class, props: Record>): Array<() => void> { const subs: Array<() => void> = []; const isGObject = klass instanceof GObject.Object; @@ -228,16 +228,52 @@ export function construct(klass: object, props: Record>): 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 Class] = v.get() as Class[keyof Class]; + if(isGObject) + klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}` + )); })); - klass[k as keyof typeof klass] = v.get() as never; + klass[k as keyof Class] = v.get() as Class[keyof Class]; return; } + - klass[k as keyof typeof klass] = v as never; + klass[k as keyof Class] = v as Class[keyof Class]; }); return subs; } + +/** open connections to gobjects that are closed when the scope +* is disposed +* @experimental +* */ +export function createConnetions< + GObj extends GObject.Object, + Signals extends GObj["$signals"], + Signal extends keyof Signals, + Callback extends Signals[Signal] +>(...conns: Array<[GObj, Signal, Callback]>): void { + const scope = getScope(); + + const connections: Map> = new Map(); + + scope.onCleanup(() => connections.forEach((ids, gobj) => + ids.forEach(id => gobj.disconnect(id)) + )); + + function add(gobj: GObj, id: number): void { + if(connections.has(gobj)) { + connections.get(gobj)!.push(id); + return; + } + + connections.set(gobj, [id]); + } + + conns.forEach(([gobj, sig, callback]) => { + // type stuff + add(gobj, gobj.connect(sig as string, callback as never)); + }); +} diff --git a/src/widget/bar/Status.tsx b/src/widget/bar/Status.tsx index c35d955..6633330 100644 --- a/src/widget/bar/Status.tsx +++ b/src/widget/bar/Status.tsx @@ -10,6 +10,7 @@ import GObject from "ags/gobject"; import AstalBluetooth from "gi://AstalBluetooth"; import AstalNetwork from "gi://AstalNetwork"; import AstalWp from "gi://AstalWp"; +import { Bluetooth } from "../../modules/bluetooth"; export const Status = () => @@ -22,14 +23,16 @@ export const Status = () => !Wireplumber.getDefault().isMutedSink() && - Wireplumber.getDefault().getSinkVolume() > 0 ? icon + Wireplumber.getDefault().getSinkVolume() > 0 ? + icon : "audio-volume-muted-symbolic") } /> !Wireplumber.getDefault().isMutedSource() && - Wireplumber.getDefault().getSourceVolume() > 0 ? icon + Wireplumber.getDefault().getSourceVolume() > 0 ? + icon : "microphone-sensitivity-muted-symbolic") } /> @@ -40,22 +43,9 @@ export const Status = () => - { - if(!recording || !Recording.getDefault().startedAt) - return "..."; - - const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!; - if(startedAtSeconds <= 0) return "00:00"; - - const minutes = Math.floor(startedAtSeconds / 60); - const seconds = Math.floor(startedAtSeconds % 60); - - return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`; - })} - /> + @@ -99,7 +89,7 @@ function StatusIcons() { : "bluetooth-symbolic" ) : "bluetooth-disabled-symbolic" })} class={"bluetooth state"} visible={ - createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean) + createBinding(Bluetooth.getDefault(), "adapter").as(Boolean) } /> diff --git a/src/widget/control-center/pages/Bluetooth.tsx b/src/widget/control-center/pages/Bluetooth.tsx index bed9c73..38def28 100644 --- a/src/widget/control-center/pages/Bluetooth.tsx +++ b/src/widget/control-center/pages/Bluetooth.tsx @@ -4,10 +4,13 @@ import { tr } from "../../../i18n/intl"; import { Windows } from "../../../windows"; import { Notifications } from "../../../modules/notifications"; import { execApp } from "../../../modules/apps"; +import { execAsync } from "ags/process"; import { createBinding, createComputed, For, With } from "ags"; +import { Bluetooth } from "../../../modules/bluetooth"; import AstalNotifd from "gi://AstalNotifd"; import AstalBluetooth from "gi://AstalBluetooth"; +import Adw from "gi://Adw?version=1"; export const BluetoothPage = new Page({ @@ -15,27 +18,27 @@ export const BluetoothPage = new Page({ title: tr("control_center.pages.bluetooth.title"), spacing: 6, description: tr("control_center.pages.bluetooth.description"), - headerButtons: [{ - icon: createBinding(AstalBluetooth.get_default().adapter, "discovering") + headerButtons: createBinding(Bluetooth.getDefault(), "adapter").as(adapter => adapter ? [{ + icon: createBinding(adapter, "discovering") .as(discovering => !discovering ? "arrow-circular-top-right-symbolic" : "media-playback-stop-symbolic" ), - tooltipText: createBinding(AstalBluetooth.get_default().adapter, "discovering") + tooltipText: createBinding(adapter, "discovering") .as((discovering) => !discovering ? tr("control_center.pages.bluetooth.start_discovering") : tr("control_center.pages.bluetooth.stop_discovering")), actionClicked: () => { - if(AstalBluetooth.get_default().adapter.discovering) { - AstalBluetooth.get_default().adapter.stop_discovery(); + if(adapter.discovering) { + adapter.stop_discovery(); return; } - AstalBluetooth.get_default().adapter.start_discovery(); + adapter.start_discovery(); } - }], - actionClosed: () => AstalBluetooth.get_default().adapter?.discovering && - AstalBluetooth.get_default().adapter.stop_discovery(), + }]: []), + actionClosed: () => Bluetooth.getDefault().adapter?.discovering && + Bluetooth.getDefault().adapter?.stop_discovery(), bottomButtons: [{ title: tr("control_center.pages.more_settings"), actionClicked: () => { @@ -43,63 +46,79 @@ export const BluetoothPage = new Page({ execApp("overskride", "[float; animation slide right]"); } }], - content: () => [ - adptrs.length > 1) - } spacing={2} orientation={Gtk.Orientation.VERTICAL}> + content: () => { + const adapter = createBinding(Bluetooth.getDefault(), "adapter"); + const adapters = createBinding(AstalBluetooth.get_default(), "adapters"); + const devices = createBinding(AstalBluetooth.get_default(), "devices"); - - - adpts.length > 1)}> + return [ + adptrs.length > 1) + } spacing={2} orientation={Gtk.Orientation.VERTICAL}> - {(hasMoreAdapters: boolean) => hasMoreAdapters && - - - {(adapter: AstalBluetooth.Adapter) => { - const isSelected = createBinding(AstalBluetooth.get_default(), "adapter").as(a => - a.address === adapter.address); - - return is ? "selected" : "")} - title={adapter.alias ?? "Adapter"} icon={"bluetooth-active-symbolic"} - description={createBinding(adapter, "address")} - endWidget={ - - } - />; - }} - - - } - - , - - - - devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0)}> - - - - devs.filter(dev => dev.paired || dev.connected || dev.trusted))}> - - {(dev: AstalBluetooth.Device) => } - - - - devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted).length > 0)}> - - - - devs.filter(dev => !dev.connected && !dev.paired && !dev.trusted))}> - {(dev: AstalBluetooth.Device) => } - + adpts.length > 1)}> + {(hasMoreAdapters: boolean) => hasMoreAdapters && + + + {(adapter: AstalBluetooth.Adapter) => { + const isSelected = createBinding(Bluetooth.getDefault(), "adapter").as(a => + adapter.address === a?.address); + + return is ? "selected" : "")} + title={adapter.alias ?? "Adapter"} icon={"bluetooth-active-symbolic"} + description={createBinding(adapter, "address")} + actionClicked={() => + adapter.address !== Bluetooth.getDefault().adapter?.address && + selectAdapter(adapter) + } + endWidget={ + + } + />; + }} + + + } + + , + + + devs.filter(dev => + (dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address && + dev.paired || dev.connected || dev.trusted).length > 0) + }> + + + devs.filter(dev => + (dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address && + dev.paired || dev.connected || dev.trusted)) + }> + + {(dev: AstalBluetooth.Device) => } + + + devs.filter(dev => + (dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address && + !dev.connected && !dev.paired && !dev.trusted).length > 0) + }> + + + devs.filter(dev => + (dev.adapter as AstalBluetooth.Adapter).address === adapter.get()?.address && + !dev.connected && !dev.paired && !dev.trusted)) + }> + + {(dev: AstalBluetooth.Device) => } + + - - ] + ]; + } }); function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget { @@ -107,9 +126,6 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget conn ? "selected" : "")} title={ createBinding(device, "alias").as(alias => alias ?? "Unknown Device")} icon={createBinding(device, "icon").as(ico => ico ?? "bluetooth-active-symbolic")} - description={ - createBinding(device, "connecting").as(connecting => - connecting ? `${tr("connecting")}...` : "")} tooltipText={ createBinding(device, "connected").as(connected => !connected ? tr("connect") : "") @@ -138,19 +154,24 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget }) ); }} - endWidget={ connected && (batt > -1)) - }> - - `${Math.floor(batt * 100)}%`)} /> + endWidget={ + + connected && (batt > -1)) + } spacing={4}> + + `${Math.floor(batt * 100)}%`) + } visible={createBinding(device, "connected")} + /> - - `battery-level-${Math.floor(batt * 100)}-symbolic`) - } css={"font-size: 16px; margin-left: 6px;"} /> + + `battery-level-${Math.floor(batt * 100)}-symbolic`) + } css={"font-size: 16px; margin-left: 6px;"} /> + } extraButtons={ { if(!connected) { - AstalBluetooth.get_default().adapter?.remove_device(device); + Bluetooth.getDefault().adapter?.remove_device(device); return; } @@ -182,3 +203,18 @@ function DeviceWidget({ device }: { device: AstalBluetooth.Device }): Gtk.Widget } /> as Gtk.Widget; } + +function selectAdapter(adapter: AstalBluetooth.Adapter): void { + AstalBluetooth.get_default().adapters.filter(ad => { + if(ad.alias !== adapter.alias) + return true; + + ad.set_powered(true); + return false; + }).forEach(ad => ad.set_powered(false)); + + execAsync(`bluetoothctl select ${adapter.address}`).catch(e => + console.error(`Bluetooth: Couldn't select adapter. Stderr: ${e}`)); + + Bluetooth.getDefault().adapter = adapter; +} diff --git a/src/widget/control-center/tiles/Bluetooth.tsx b/src/widget/control-center/tiles/Bluetooth.tsx index 9802033..3bc7514 100644 --- a/src/widget/control-center/tiles/Bluetooth.tsx +++ b/src/widget/control-center/tiles/Bluetooth.tsx @@ -3,17 +3,21 @@ import AstalBluetooth from "gi://AstalBluetooth"; import { BluetoothPage } from "../pages/Bluetooth"; import { TilesPages } from "../Tiles"; import { createBinding, createComputed } from "ags"; +import { Bluetooth } from "../../../modules/bluetooth"; export const TileBluetooth = () => { - const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0]; - return connected && connectedDev ? connectedDev.get_alias() : "" + if(!connected) return ""; + + const connectedDevs = AstalBluetooth.get_default().devices.filter(dev => dev.connected); + const connectedDev = connectedDevs[connectedDevs.length - 1]; // last connected device is on display + return connectedDev ? connectedDev.get_alias() : "" })} - onEnabled={() => AstalBluetooth.get_default().adapter?.set_powered(true)} - onDisabled={() => AstalBluetooth.get_default().adapter?.set_powered(false)} + onEnabled={() => Bluetooth.getDefault().adapter?.set_powered(true)} + onDisabled={() => Bluetooth.getDefault().adapter?.set_powered(false)} onClicked={() => TilesPages?.toggle(BluetoothPage)} enableOnClicked hasArrow state={createBinding(AstalBluetooth.get_default(), "isPowered")} diff --git a/src/window/FloatingNotifications.tsx b/src/window/FloatingNotifications.tsx index bbad256..43e511e 100644 --- a/src/window/FloatingNotifications.tsx +++ b/src/window/FloatingNotifications.tsx @@ -1,16 +1,50 @@ import { Astal, Gtk } from "ags/gtk4"; -import { createBinding, For } from "ags"; +import { createBinding, createComputed, For } from "ags"; import { Notifications } from "../modules/notifications"; import { NotificationWidget } from "../widget/Notification"; +import { generalConfig } from "../app"; -import AstalNotifd from "gi://AstalNotifd?version=0.1"; +import AstalNotifd from "gi://AstalNotifd"; import Adw from "gi://Adw?version=1"; const size = 450; export const FloatingNotifications = (mon: number) => { + let horizontal: Astal.WindowAnchor = Astal.WindowAnchor.RIGHT, + vertical: Astal.WindowAnchor = Astal.WindowAnchor.TOP; + + switch(posH) { + case "left": + horizontal = Astal.WindowAnchor.LEFT; + break; + case "center": + horizontal = Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + break; + case "right": + horizontal = Astal.WindowAnchor.RIGHT; + break; + } + + switch(posV) { + case "top": + vertical = Astal.WindowAnchor.TOP; + break; + case "center": + vertical = Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM; + break; + case "bottom": + vertical = Astal.WindowAnchor.BOTTOM; + break; + } + + return horizontal | vertical; + + })} exclusivity={Astal.Exclusivity.NORMAL} resizable={false} widthRequest={450}> {(notif: AstalNotifd.Notification) => - - + { + //TODO: support different animations depending on screen position + return Gtk.StackTransitionType.SLIDE_RIGHT + })} transitionDuration={300}> + + - Notifications.getDefault().removeNotification(notif)} - holdOnHover actionClicked={() => { - const viewAction = notif.actions.filter(a => - a.id.toLowerCase() === "view" || - a.label.toLowerCase() === "view" - )?.[0]; + Notifications.getDefault().removeNotification(notif)} + holdOnHover actionClicked={() => { + const viewAction = notif.actions.filter(a => + a.id.toLowerCase() === "view" || + a.label.toLowerCase() === "view" + )?.[0]; - viewAction && notif.invoke(viewAction.id); - }} - /> - - + viewAction && notif.invoke(viewAction.id); + }} + /> + + as Gtk.Widget + }> + + }