From c41493373af18bd1c7c9b25882c93c1841110baa Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 7 Nov 2024 01:27:21 +0100 Subject: example: notifications --- examples/js/notifications/.gitignore | 2 + examples/js/notifications/README.md | 3 + examples/js/notifications/app.ts | 9 ++ .../notifications/notifications/Notification.scss | 125 +++++++++++++++++++++ .../notifications/notifications/Notification.tsx | 107 ++++++++++++++++++ .../notifications/NotificationPopups.tsx | 115 +++++++++++++++++++ examples/js/notifications/style.scss | 1 + 7 files changed, 362 insertions(+) create mode 100644 examples/js/notifications/.gitignore create mode 100644 examples/js/notifications/README.md create mode 100644 examples/js/notifications/app.ts create mode 100644 examples/js/notifications/notifications/Notification.scss create mode 100644 examples/js/notifications/notifications/Notification.tsx create mode 100644 examples/js/notifications/notifications/NotificationPopups.tsx create mode 100644 examples/js/notifications/style.scss (limited to 'examples/js/notifications') diff --git a/examples/js/notifications/.gitignore b/examples/js/notifications/.gitignore new file mode 100644 index 0000000..6850183 --- /dev/null +++ b/examples/js/notifications/.gitignore @@ -0,0 +1,2 @@ +@girs/ +node_modules/ \ No newline at end of file diff --git a/examples/js/notifications/README.md b/examples/js/notifications/README.md new file mode 100644 index 0000000..7d7562a --- /dev/null +++ b/examples/js/notifications/README.md @@ -0,0 +1,3 @@ +# Notifications Popups + +A replacement for dunst and other daemons using [Notifd](https://aylur.github.io/astal/guide/libraries/notifd). diff --git a/examples/js/notifications/app.ts b/examples/js/notifications/app.ts new file mode 100644 index 0000000..ed53292 --- /dev/null +++ b/examples/js/notifications/app.ts @@ -0,0 +1,9 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" +import NotificationPopups from "./notifications/NotificationPopups" + +App.start({ + instanceName: "notifications", + css: style, + main: () => App.get_monitors().map(NotificationPopups), +}) diff --git a/examples/js/notifications/notifications/Notification.scss b/examples/js/notifications/notifications/Notification.scss new file mode 100644 index 0000000..c902939 --- /dev/null +++ b/examples/js/notifications/notifications/Notification.scss @@ -0,0 +1,125 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &:first-child>box { + margin-top: 1rem; + } + + &:last-child>box { + margin-bottom: 1rem; + } + + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); + } + + &.critical>box { + border: 1pt solid gtkalpha($error, .4); + + .header { + + .app-name { + color: gtkalpha($error, .8); + + } + + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/examples/js/notifications/notifications/Notification.tsx b/examples/js/notifications/notifications/Notification.tsx new file mode 100644 index 0000000..5149d5b --- /dev/null +++ b/examples/js/notifications/notifications/Notification.tsx @@ -0,0 +1,107 @@ +import { GLib } from "astal" +import { Gtk, Astal } from "astal/gtk3" +import { type EventBox } from "astal/gtk3/widget" +import Notifd from "gi://AstalNotifd" + +const isIcon = (icon: string) => + !!Astal.Icon.lookup_icon(icon) + +const fileExists = (path: string) => + GLib.file_test(path, GLib.FileTest.EXISTS) + +const time = (time: number, format = "%H:%M") => GLib.DateTime + .new_from_unix_local(time) + .format(format)! + +const urgency = (n: Notifd.Notification) => { + const { LOW, NORMAL, CRITICAL } = Notifd.Urgency + // match operator when? + switch (n.urgency) { + case LOW: return "low" + case CRITICAL: return "critical" + case NORMAL: + default: return "normal" + } +} + +type Props = { + setup(self: EventBox): void + onHoverLost(self: EventBox): void + notification: Notifd.Notification +} + +export default function Notification(props: Props) { + const { notification: n, onHoverLost, setup } = props + const { START, CENTER, END } = Gtk.Align + + return + + + {(n.appIcon || n.desktopEntry) && } + + + + {n.image && fileExists(n.image) && } + {n.image && isIcon(n.image) && + + } + + + + {n.get_actions().length > 0 && + {n.get_actions().map(({ label, id }) => ( + + ))} + } + + +} diff --git a/examples/js/notifications/notifications/NotificationPopups.tsx b/examples/js/notifications/notifications/NotificationPopups.tsx new file mode 100644 index 0000000..399e3e0 --- /dev/null +++ b/examples/js/notifications/notifications/NotificationPopups.tsx @@ -0,0 +1,115 @@ +import { Astal, Gtk, Gdk } from "astal/gtk3" +import Notifd from "gi://AstalNotifd" +import Notification from "./Notification" +import { type Subscribable } from "astal/binding" +import { GLib, Variable, bind, timeout } from "astal" + +// see comment below in constructor +const TIMEOUT_DELAY = 5000 + +// The purpose if this class is to replace Variable> +// with a Map type in order to track notification widgets +// by their id, while making it conviniently bindable as an array +class NotifiationMap implements Subscribable { + // it makes sense to share a single instance across all monitors + static get_default() { + if (!this.instance) + this.instance = new NotifiationMap() + + return this.instance + } + + private static instance: NotifiationMap + + // the underlying map to keep track of id widget pairs + private map: Map = new Map() + + // it makes sense to use a Variable under the hood and use its + // reactivity implementation instead of keeping track of subscribers ourselves + private var: Variable> = Variable([]) + + // notify subscribers to rerender when state changes + private notifiy() { + this.var.set([...this.map.values()].reverse()) + } + + private constructor() { + const notifd = Notifd.get_default() + + /** + * uncomment this if you want to + * ignore timeout by senders and enforce our own timeout + * note that if the notification has any actions + * they might not work, since the sender already treats them as resolved + */ + // notifd.ignoreTimeout = true + + notifd.connect("notified", (_, id) => { + this.set(id, Notification({ + notification: notifd.get_notification(id)!, + + // once hovering over the notification is done + // destroy the widget without calling notification.dismiss() + // so that it acts as a "popup" and we can still display it + // in a notification center like widget + // but clicking on the close button will close it + onHoverLost: () => this.delete(id), + + // notifd by default does not close notifications + // until user input or the timeout specified by sender + // which we set to ignore above + setup: () => timeout(TIMEOUT_DELAY, () => { + /** + * uncomment this if you want to "hide" the notifications + * after TIMEOUT_DELAY + */ + // this.delete(id) + }) + })) + }) + + // notifications can be closed by the outside before + // any user input, which have to be handled too + notifd.connect("resolved", (_, id) => { + this.delete(id) + }) + } + + private set(key: number, value: Gtk.Widget) { + // in case of replacecment destroy previous widget + this.map.get(key)?.destroy() + this.map.set(key, value) + this.notifiy() + } + + private delete(key: number) { + this.map.get(key)?.destroy() + this.map.delete(key) + this.notifiy() + } + + // needed by the Subscribable interface + get() { + return this.var.get() + } + + // needed by the Subscribable interface + subscribe(callback: (list: Array) => void) { + return this.var.subscribe(callback) + } +} + +export default function NotificationPopups(gdkmonitor: Gdk.Monitor) { + const { TOP, RIGHT } = Astal.WindowAnchor + const notifs = NotifiationMap.get_default() + + return + + {bind(notifs)} + + +} diff --git a/examples/js/notifications/style.scss b/examples/js/notifications/style.scss new file mode 100644 index 0000000..7ef0168 --- /dev/null +++ b/examples/js/notifications/style.scss @@ -0,0 +1 @@ +@use "./notifications/Notification.scss"; -- cgit v1.2.3 From 12ee15f4dadc0c006a8e104ef0b45afebd29fcc7 Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 7 Nov 2024 01:39:01 +0100 Subject: docs: showcase notification example --- examples/js/notifications/README.md | 2 ++ 1 file changed, 2 insertions(+) (limited to 'examples/js/notifications') diff --git a/examples/js/notifications/README.md b/examples/js/notifications/README.md index 7d7562a..60dad60 100644 --- a/examples/js/notifications/README.md +++ b/examples/js/notifications/README.md @@ -1,3 +1,5 @@ # Notifications Popups +![notifs](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44) + A replacement for dunst and other daemons using [Notifd](https://aylur.github.io/astal/guide/libraries/notifd). -- cgit v1.2.3 From daee063c8e713468806c25d27e7c6fcff340a41f Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 7 Nov 2024 01:04:01 +0000 Subject: fix: notification example correction: no, it does not make sense to make it a singleton --- .../js/notifications/notifications/NotificationPopups.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) (limited to 'examples/js/notifications') diff --git a/examples/js/notifications/notifications/NotificationPopups.tsx b/examples/js/notifications/notifications/NotificationPopups.tsx index 399e3e0..a8088c9 100644 --- a/examples/js/notifications/notifications/NotificationPopups.tsx +++ b/examples/js/notifications/notifications/NotificationPopups.tsx @@ -11,16 +11,6 @@ const TIMEOUT_DELAY = 5000 // with a Map type in order to track notification widgets // by their id, while making it conviniently bindable as an array class NotifiationMap implements Subscribable { - // it makes sense to share a single instance across all monitors - static get_default() { - if (!this.instance) - this.instance = new NotifiationMap() - - return this.instance - } - - private static instance: NotifiationMap - // the underlying map to keep track of id widget pairs private map: Map = new Map() @@ -101,7 +91,7 @@ class NotifiationMap implements Subscribable { export default function NotificationPopups(gdkmonitor: Gdk.Monitor) { const { TOP, RIGHT } = Astal.WindowAnchor - const notifs = NotifiationMap.get_default() + const notifs = new NotifiationMap() return Date: Thu, 7 Nov 2024 01:11:51 +0000 Subject: style: indent example scss --- .../notifications/notifications/Notification.scss | 178 ++++++++++----------- 1 file changed, 89 insertions(+), 89 deletions(-) (limited to 'examples/js/notifications') diff --git a/examples/js/notifications/notifications/Notification.scss b/examples/js/notifications/notifications/Notification.scss index c902939..a32f08b 100644 --- a/examples/js/notifications/notifications/Notification.scss +++ b/examples/js/notifications/notifications/Notification.scss @@ -1,7 +1,7 @@ @use "sass:string"; @function gtkalpha($c, $a) { - @return string.unquote("alpha(#{$c},#{$a})"); + @return string.unquote("alpha(#{$c},#{$a})"); } // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss @@ -10,116 +10,116 @@ $bg-color: #{"@theme_bg_color"}; $error: red; window.NotificationPopups { - all: unset; + all: unset; } eventbox.Notification { - &:first-child>box { - margin-top: 1rem; - } - - &:last-child>box { - margin-bottom: 1rem; - } - - // eventboxes can not take margins so we style its inner box instead - >box { - min-width: 400px; - border-radius: 13px; - background-color: $bg-color; - margin: .5rem 1rem .5rem 1rem; - box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); - border: 1pt solid gtkalpha($fg-color, .03); - } - - &.critical>box { - border: 1pt solid gtkalpha($error, .4); - - .header { - - .app-name { - color: gtkalpha($error, .8); - - } - - .app-icon { - color: gtkalpha($error, .6); - } + &:first-child>box { + margin-top: 1rem; } - } - .header { - padding: .5rem; - color: gtkalpha($fg-color, 0.5); - - .app-icon { - margin: 0 .4rem; + &:last-child>box { + margin-bottom: 1rem; } - .app-name { - margin-right: .3rem; - font-weight: bold; - - &:first-child { - margin-left: .4rem; - } + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); } - .time { - margin: 0 .4rem; - } + &.critical>box { + border: 1pt solid gtkalpha($error, .4); - button { - padding: .2rem; - min-width: 0; - min-height: 0; - } - } + .header { - separator { - margin: 0 .4rem; - background-color: gtkalpha($fg-color, .1); - } + .app-name { + color: gtkalpha($error, .8); - .content { - margin: 1rem; - margin-top: .5rem; + } - .summary { - font-size: 1.2em; - color: $fg-color; + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } } - .body { - color: gtkalpha($fg-color, 0.8); + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); } - .image { - border: 1px solid gtkalpha($fg-color, .02); - margin-right: .5rem; - border-radius: 9px; - min-width: 100px; - min-height: 100px; - background-size: cover; - background-position: center; + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } } - } - .actions { - margin: 1rem; - margin-top: 0; + .actions { + margin: 1rem; + margin-top: 0; - button { - margin: 0 .3rem; + button { + margin: 0 .3rem; - &:first-child { - margin-left: 0; - } + &:first-child { + margin-left: 0; + } - &:last-child { - margin-right: 0; - } + &:last-child { + margin-right: 0; + } + } } - } } -- cgit v1.2.3 From 779ec440e209b01d3a44f9377af78653d197fdc8 Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 7 Nov 2024 02:40:47 +0000 Subject: example: applauncher --- examples/js/notifications/.gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 examples/js/notifications/.gitignore (limited to 'examples/js/notifications') diff --git a/examples/js/notifications/.gitignore b/examples/js/notifications/.gitignore deleted file mode 100644 index 6850183..0000000 --- a/examples/js/notifications/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -@girs/ -node_modules/ \ No newline at end of file -- cgit v1.2.3