summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-11-07 01:27:21 +0100
committerAylur <[email protected]>2024-11-07 00:28:39 +0000
commitc41493373af18bd1c7c9b25882c93c1841110baa (patch)
treec6983eb5bf1d448cc094c10a897bc2e043f0cb1e /examples
parent84c02e54d3bd25958dafa67a7420cf29b3375de1 (diff)
example: notifications
Diffstat (limited to 'examples')
-rw-r--r--examples/js/notifications/.gitignore2
-rw-r--r--examples/js/notifications/README.md3
-rw-r--r--examples/js/notifications/app.ts9
-rw-r--r--examples/js/notifications/notifications/Notification.scss125
-rw-r--r--examples/js/notifications/notifications/Notification.tsx107
-rw-r--r--examples/js/notifications/notifications/NotificationPopups.tsx115
-rw-r--r--examples/js/notifications/style.scss1
7 files changed, 362 insertions, 0 deletions
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 <eventbox
+ className={`Notification ${urgency(n)}`}
+ setup={setup}
+ onHoverLost={onHoverLost}>
+ <box vertical>
+ <box className="header">
+ {(n.appIcon || n.desktopEntry) && <icon
+ className="app-icon"
+ visible={Boolean(n.appIcon || n.desktopEntry)}
+ icon={n.appIcon || n.desktopEntry}
+ />}
+ <label
+ className="app-name"
+ halign={START}
+ truncate
+ label={n.appName || "Unknown"}
+ />
+ <label
+ className="time"
+ hexpand
+ halign={END}
+ label={time(n.time)}
+ />
+ <button onClicked={() => n.dismiss()}>
+ <icon icon="window-close-symbolic" />
+ </button>
+ </box>
+ <Gtk.Separator visible />
+ <box className="content">
+ {n.image && fileExists(n.image) && <box
+ valign={START}
+ className="image"
+ css={`background-image: url('${n.image}')`}
+ />}
+ {n.image && isIcon(n.image) && <box
+ expand={false}
+ valign={START}
+ className="icon-image">
+ <icon icon={n.image} expand halign={CENTER} valign={CENTER} />
+ </box>}
+ <box vertical>
+ <label
+ className="summary"
+ halign={START}
+ xalign={0}
+ label={n.summary}
+ truncate
+ />
+ {n.body && <label
+ className="body"
+ wrap
+ useMarkup
+ halign={START}
+ xalign={0}
+ justifyFill
+ label={n.body}
+ />}
+ </box>
+ </box>
+ {n.get_actions().length > 0 && <box className="actions">
+ {n.get_actions().map(({ label, id }) => (
+ <button
+ hexpand
+ onClicked={() => n.invoke(id)}>
+ <label label={label} halign={CENTER} hexpand />
+ </button>
+ ))}
+ </box>}
+ </box>
+ </eventbox>
+}
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<Array<Widget>>
+// with a Map<number, Widget> 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<number, Gtk.Widget> = 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<Array<Gtk.Widget>> = 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<Gtk.Widget>) => void) {
+ return this.var.subscribe(callback)
+ }
+}
+
+export default function NotificationPopups(gdkmonitor: Gdk.Monitor) {
+ const { TOP, RIGHT } = Astal.WindowAnchor
+ const notifs = NotifiationMap.get_default()
+
+ return <window
+ className="NotificationPopups"
+ gdkmonitor={gdkmonitor}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ anchor={TOP | RIGHT}>
+ <box vertical>
+ {bind(notifs)}
+ </box>
+ </window>
+}
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";