From 13b0c8cc288ad26f500f22d2ea2f419d8c4ca272 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:22:58 +0200 Subject: [PATCH 01/21] feat-wip: Scaffold timemenu window --- ags/bar/widgets/time.ts | 15 +++++++-------- ags/main.ts | 7 ++++--- ags/style.scss | 8 +++++++- ags/timemenu/notification-list.ts | 25 +++++++++++++++++++++++++ ags/timemenu/timemenu.ts | 17 +++++++++++++++++ 5 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 ags/timemenu/notification-list.ts create mode 100644 ags/timemenu/timemenu.ts diff --git a/ags/bar/widgets/time.ts b/ags/bar/widgets/time.ts index 3424d87..68f3ce3 100644 --- a/ags/bar/widgets/time.ts +++ b/ags/bar/widgets/time.ts @@ -5,11 +5,10 @@ const time = Variable("", { poll: [1000, 'date +"%-d %b %H:%M"'], }); -export const Time = () => - BarWidget({ - child: Widget.Label({ - className: "time", - label: time.bind(), - }), - onClicked: () => Utils.execAsync("swaync-client -t"), - }); +export const Time = () => BarWidget({ + child: Widget.Label({ + className: "time", + label: time.bind(), + }), + on_clicked: () => App.toggleWindow('timemenu'), +}); diff --git a/ags/main.ts b/ags/main.ts index 859b847..c359dd5 100644 --- a/ags/main.ts +++ b/ags/main.ts @@ -1,8 +1,9 @@ import Gdk from "gi://Gdk"; import type Gtk from "gi://Gtk?version=3.0"; -import { Bar } from "./bar/bar.js"; -import { Quicksettings } from "./quicksettings/quicksettings.js"; +import { Quicksettings } from './quicksettings/quicksettings.js'; +import TimeMenu from './timemenu/timemenu'; +import { Bar } from './bar/bar.js'; function forMonitors(widget: (monitor: number) => Gtk.Window) { const n = Gdk.Display.get_default()?.get_n_monitors() || 1; @@ -12,6 +13,6 @@ function forMonitors(widget: (monitor: number) => Gtk.Window) { export function main(dest: string): void { App.config({ style: `${dest}/style.css`, - windows: [...forMonitors(Bar), Quicksettings()], + windows: [...forMonitors(Bar), Quicksettings(), TimeMenu()], }); } diff --git a/ags/style.scss b/ags/style.scss index d60527f..2542f1c 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -340,5 +340,11 @@ window.darkened { } } } +} + +.timemenu { + @include rounded-full; -} \ No newline at end of file + padding: 2em; + margin: 2em; +} diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts new file mode 100644 index 0000000..f438ee7 --- /dev/null +++ b/ags/timemenu/notification-list.ts @@ -0,0 +1,25 @@ +import { type Notification as NotificationInfo } from "types/service/notifications"; + +const notificationService = await Service.import("notifications"); +const notifications = notificationService.bind("notifications"); + +const Notification = (info: NotificationInfo) => { + +}; + +const Placeholder = () => Widget.Box({ + visible: notifications.as(n => n.length === 0), + children: [ + Widget.Icon("notifications-disabled-symbolic"), + Widget.Label("Your inbox is empty"), + ], +}); + +const NotificationList = () => Widget.Box({ +}); + +export default () => Widget.Box({ + children: [ + Placeholder(), + ], +}); diff --git a/ags/timemenu/timemenu.ts b/ags/timemenu/timemenu.ts new file mode 100644 index 0000000..a1ce114 --- /dev/null +++ b/ags/timemenu/timemenu.ts @@ -0,0 +1,17 @@ +import NotificationList from './notification-list'; + +export default () => Widget.Window({ + visible: false, + name: "timemenu", + className: "timemenu", + anchor: ['top'], + layer: "top", + exclusivity: "normal", + setup: w => w.keybind("Escape", () => App.closeWindow("timemenu")), + child: Widget.Box({ + vertical: true, + children: [ + NotificationList(), + ], + }), +}); From d62adeeb97cd729d077d2e2ce3125c69770bfa8b Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:55:23 +0200 Subject: [PATCH 02/21] feat-wip: Struggle with css (HARD) --- ags/style.scss | 14 ++++- ags/timemenu/notification-list.ts | 68 +++++++++++++++++++++-- ags/timemenu/notification.ts | 91 +++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 ags/timemenu/notification.ts diff --git a/ags/style.scss b/ags/style.scss index 2542f1c..f3dba15 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -345,6 +345,16 @@ window.darkened { .timemenu { @include rounded-full; - padding: 2em; - margin: 2em; + // padding: 2em; + // margin: 2em; + border-radius: 1.5em; + + .notification-scrollable { + min-height: 5em; + } + + .notification { + min-width: 50em; + min-height: 5em; + } } diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index f438ee7..09b5d1d 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -1,25 +1,83 @@ import { type Notification as NotificationInfo } from "types/service/notifications"; +import Notification from "./notification"; + const notificationService = await Service.import("notifications"); const notifications = notificationService.bind("notifications"); -const Notification = (info: NotificationInfo) => { - -}; +const Animated = (n: NotificationInfo) => Widget.Revealer({ + transition_duration: 200, + transition: "slide_down", + child: Notification(n), + setup: self => Utils.timeout(200, () => { + if (!self.is_destroyed) + self.reveal_child = true; + }), +}); const Placeholder = () => Widget.Box({ visible: notifications.as(n => n.length === 0), + hpack: "center", + vpack: "center", children: [ Widget.Icon("notifications-disabled-symbolic"), Widget.Label("Your inbox is empty"), ], }); -const NotificationList = () => Widget.Box({ -}); +const NotificationList = () => { + const map: Map> = new Map + let box = Widget.Box({ + vertical: true, + children: notificationService.notifications.map(n => { + const w = Animated(n) + map.set(n.id, w) + return w + }), + visible: notifications.as(n => n.length > 0), + }) + + function remove(_: unknown, id: number) { + const n = map.get(id) + if (n) { + n.reveal_child = false + Utils.timeout(200, () => { + n.destroy() + map.delete(id) + }) + } + } + + box = box + .hook(notificationService, remove, "closed") + .hook(notificationService, (_, id: number) => { + if (id !== undefined) { + if (map.has(id)) + remove(null, id) + + const n = notificationService.getNotification(id)! + + const w = Animated(n) + map.set(id, w) + box.children = [w, ...box.children] + } + }, "notified") + + return Widget.Box({ + children: [ + Widget.Scrollable({ + class_name: "notifications-scrollable", + vexpand: true, + hscroll: "never", + child: box + }) + ], + }) +}; export default () => Widget.Box({ children: [ Placeholder(), + NotificationList(), ], }); diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts new file mode 100644 index 0000000..402b179 --- /dev/null +++ b/ags/timemenu/notification.ts @@ -0,0 +1,91 @@ +import { type Notification as NotificationInfo } from "types/service/notifications" + +export default (notification: NotificationInfo) => { + const content = Widget.Box({ + class_name: "content", + children: [ + // NotificationIcon(notification), + Widget.Box({ + hexpand: true, + vertical: true, + children: [ + Widget.Box({ + children: [ + Widget.Label({ + class_name: "title", + xalign: 0, + justification: "left", + hexpand: true, + max_width_chars: 24, + truncate: "end", + wrap: true, + label: notification.summary.trim(), + use_markup: true, + }), + // Widget.Label({ + // class_name: "time", + // vpack: "start", + // label: time(notification.time), + // }), + Widget.Button({ + class_name: "close-button", + vpack: "start", + child: Widget.Icon("window-close-symbolic"), + on_clicked: notification.close, + }), + ], + }), + Widget.Label({ + class_name: "description", + hexpand: true, + use_markup: true, + xalign: 0, + justification: "left", + label: notification.body.trim(), + max_width_chars: 24, + wrap: true, + }), + ], + }), + ], + }) + + const actionsbox = notification.actions.length > 0 ? Widget.Revealer({ + transition: "slide_down", + child: Widget.EventBox({ + child: Widget.Box({ + class_name: "actions horizontal", + children: notification.actions.map(action => Widget.Button({ + class_name: "action-button", + on_clicked: () => notification.invoke(action.id), + hexpand: true, + child: Widget.Label(action.label), + })), + }), + }), + }) : null + + const eventbox = Widget.EventBox({ + vexpand: false, + on_primary_click: notification.dismiss, + on_hover() { + if (actionsbox) + actionsbox.reveal_child = true + }, + on_hover_lost() { + if (actionsbox) + actionsbox.reveal_child = true + + notification.dismiss() + }, + child: Widget.Box({ + vertical: true, + children: actionsbox ? [content, actionsbox] : [content], + }), + }) + + return Widget.Box({ + class_name: `notification ${notification.urgency}`, + child: eventbox, + }) +} From 6441bff4df2d03e2390ed775652c37afd0b52e3a Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:54:46 +0100 Subject: [PATCH 03/21] feat-wip(notification-menu): Use popup window helper --- ags/bar/widgets/time.ts | 15 ++-- ags/lib/widgets.ts | 1 - ags/main.ts | 6 +- ags/style.scss | 17 ++--- ags/timemenu/notification-list.ts | 114 +++++++++++------------------- ags/timemenu/notification.ts | 55 +++++++------- ags/timemenu/timemenu.ts | 31 ++++---- 7 files changed, 102 insertions(+), 137 deletions(-) diff --git a/ags/bar/widgets/time.ts b/ags/bar/widgets/time.ts index 68f3ce3..ef16647 100644 --- a/ags/bar/widgets/time.ts +++ b/ags/bar/widgets/time.ts @@ -5,10 +5,11 @@ const time = Variable("", { poll: [1000, 'date +"%-d %b %H:%M"'], }); -export const Time = () => BarWidget({ - child: Widget.Label({ - className: "time", - label: time.bind(), - }), - on_clicked: () => App.toggleWindow('timemenu'), -}); +export const Time = () => + BarWidget({ + child: Widget.Label({ + className: "time", + label: time.bind(), + }), + on_clicked: () => App.toggleWindow("timemenu"), + }); diff --git a/ags/lib/widgets.ts b/ags/lib/widgets.ts index 722ec38..68633b0 100644 --- a/ags/lib/widgets.ts +++ b/ags/lib/widgets.ts @@ -1,5 +1,4 @@ import type Gtk from "gi://Gtk?version=3.0"; -import { Binding } from "types/service"; /** Return only the children that are not equal to null. */ export function conditionalChildren(children: (Gtk.Widget | null)[]): Gtk.Widget[] { diff --git a/ags/main.ts b/ags/main.ts index c359dd5..e8d6f48 100644 --- a/ags/main.ts +++ b/ags/main.ts @@ -1,9 +1,9 @@ import Gdk from "gi://Gdk"; import type Gtk from "gi://Gtk?version=3.0"; -import { Quicksettings } from './quicksettings/quicksettings.js'; -import TimeMenu from './timemenu/timemenu'; -import { Bar } from './bar/bar.js'; +import { Quicksettings } from "./quicksettings/quicksettings.js"; +import TimeMenu from "./timemenu/timemenu"; +import { Bar } from "./bar/bar.js"; function forMonitors(widget: (monitor: number) => Gtk.Window) { const n = Gdk.Display.get_default()?.get_n_monitors() || 1; diff --git a/ags/style.scss b/ags/style.scss index f3dba15..31d33ca 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -343,18 +343,11 @@ window.darkened { } .timemenu { - @include rounded-full; - - // padding: 2em; - // margin: 2em; - border-radius: 1.5em; + @include space-between-y(1.5em); - .notification-scrollable { - min-height: 5em; - } + padding: 1em; + min-width: 24em; + min-height: 26em; - .notification { - min-width: 50em; - min-height: 5em; - } + border-radius: 1.5em; } diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index 09b5d1d..834e61b 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -1,83 +1,53 @@ -import { type Notification as NotificationInfo } from "types/service/notifications"; - import Notification from "./notification"; const notificationService = await Service.import("notifications"); const notifications = notificationService.bind("notifications"); -const Animated = (n: NotificationInfo) => Widget.Revealer({ - transition_duration: 200, - transition: "slide_down", - child: Notification(n), - setup: self => Utils.timeout(200, () => { - if (!self.is_destroyed) - self.reveal_child = true; - }), -}); - -const Placeholder = () => Widget.Box({ - visible: notifications.as(n => n.length === 0), - hpack: "center", - vpack: "center", - children: [ - Widget.Icon("notifications-disabled-symbolic"), - Widget.Label("Your inbox is empty"), - ], -}); +const Placeholder = () => + Widget.Box({ + visible: notifications.as((n) => n.length === 0), + className: "placeholder", + hpack: "center", + vpack: "center", + children: [Widget.Icon("notifications-disabled-symbolic"), Widget.Label("Your inbox is empty")], + }); const NotificationList = () => { - const map: Map> = new Map - let box = Widget.Box({ + const map: Map> = new Map(); + let listBox = Widget.Box({ vertical: true, - children: notificationService.notifications.map(n => { - const w = Animated(n) - map.set(n.id, w) - return w + children: notificationService.notifications.map((info) => { + const notification = Notification(info); + map.set(info.id, notification); + return notification; }), - visible: notifications.as(n => n.length > 0), - }) - - function remove(_: unknown, id: number) { - const n = map.get(id) - if (n) { - n.reveal_child = false - Utils.timeout(200, () => { - n.destroy() - map.delete(id) - }) - } - } - - box = box - .hook(notificationService, remove, "closed") - .hook(notificationService, (_, id: number) => { - if (id !== undefined) { - if (map.has(id)) - remove(null, id) - - const n = notificationService.getNotification(id)! - - const w = Animated(n) - map.set(id, w) - box.children = [w, ...box.children] - } - }, "notified") - - return Widget.Box({ - children: [ - Widget.Scrollable({ - class_name: "notifications-scrollable", - vexpand: true, - hscroll: "never", - child: box - }) - ], - }) + }); + + listBox = listBox + .hook(notificationService, (_, id: number) => map.delete(id), "closed") + .hook( + notificationService, + (_, id: number) => { + const info = notificationService.getNotification(id); + if (info === undefined) return; + const notification = Notification(info); + map.set(id, notification); + listBox.children = [notification, ...listBox.children]; + }, + "notified", + ); + + return Widget.Scrollable({ + className: "scrollable", + visible: notifications.as((n) => n.length > 0), + vexpand: true, + hscroll: "never", + child: listBox, + }); }; -export default () => Widget.Box({ - children: [ - Placeholder(), - NotificationList(), - ], -}); +export default () => + Widget.Box({ + className: "list", + children: [Placeholder(), NotificationList()], + }); diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 402b179..43c6252 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -1,4 +1,4 @@ -import { type Notification as NotificationInfo } from "types/service/notifications" +import type { Notification as NotificationInfo } from "types/service/notifications"; export default (notification: NotificationInfo) => { const content = Widget.Box({ @@ -12,11 +12,11 @@ export default (notification: NotificationInfo) => { Widget.Box({ children: [ Widget.Label({ - class_name: "title", + className: "title", xalign: 0, justification: "left", hexpand: true, - max_width_chars: 24, + maxWidthChars: 24, truncate: "end", wrap: true, label: notification.summary.trim(), @@ -48,44 +48,47 @@ export default (notification: NotificationInfo) => { ], }), ], - }) + }); - const actionsbox = notification.actions.length > 0 ? Widget.Revealer({ - transition: "slide_down", - child: Widget.EventBox({ - child: Widget.Box({ - class_name: "actions horizontal", - children: notification.actions.map(action => Widget.Button({ - class_name: "action-button", - on_clicked: () => notification.invoke(action.id), - hexpand: true, - child: Widget.Label(action.label), - })), - }), - }), - }) : null + const actionsbox = + notification.actions.length > 0 + ? Widget.Revealer({ + transition: "slide_down", + child: Widget.EventBox({ + child: Widget.Box({ + class_name: "actions horizontal", + children: notification.actions.map((action) => + Widget.Button({ + class_name: "action-button", + on_clicked: () => notification.invoke(action.id), + hexpand: true, + child: Widget.Label(action.label), + }), + ), + }), + }), + }) + : null; const eventbox = Widget.EventBox({ vexpand: false, on_primary_click: notification.dismiss, on_hover() { - if (actionsbox) - actionsbox.reveal_child = true + if (actionsbox) actionsbox.reveal_child = true; }, on_hover_lost() { - if (actionsbox) - actionsbox.reveal_child = true + if (actionsbox) actionsbox.reveal_child = true; - notification.dismiss() + notification.dismiss(); }, child: Widget.Box({ vertical: true, children: actionsbox ? [content, actionsbox] : [content], }), - }) + }); return Widget.Box({ class_name: `notification ${notification.urgency}`, child: eventbox, - }) -} + }); +}; diff --git a/ags/timemenu/timemenu.ts b/ags/timemenu/timemenu.ts index a1ce114..a74bbc2 100644 --- a/ags/timemenu/timemenu.ts +++ b/ags/timemenu/timemenu.ts @@ -1,17 +1,16 @@ -import NotificationList from './notification-list'; +import NotificationList from "./notification-list"; +import { PopupWindow } from "window"; -export default () => Widget.Window({ - visible: false, - name: "timemenu", - className: "timemenu", - anchor: ['top'], - layer: "top", - exclusivity: "normal", - setup: w => w.keybind("Escape", () => App.closeWindow("timemenu")), - child: Widget.Box({ - vertical: true, - children: [ - NotificationList(), - ], - }), -}); +export default () => + PopupWindow({ + name: "timemenu", + location: "top-center", + + child: Widget.Box({ + className: "timemenu popup", + vertical: true, + hexpand: false, + vexpand: false, + children: [NotificationList()], + }), + }); From a81e6df6918fd7d4a7938a72dd3b7c4f54097ae5 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Sun, 3 Nov 2024 09:14:00 +0100 Subject: [PATCH 04/21] refactor: Move setup functionality to `Box::setup` --- ags/timemenu/notification-list.ts | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index 834e61b..517e5e8 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -7,42 +7,44 @@ const Placeholder = () => Widget.Box({ visible: notifications.as((n) => n.length === 0), className: "placeholder", + hexpand: true, + vexpand: true, hpack: "center", vpack: "center", children: [Widget.Icon("notifications-disabled-symbolic"), Widget.Label("Your inbox is empty")], }); const NotificationList = () => { + // Keep track of registered notifications. const map: Map> = new Map(); - let listBox = Widget.Box({ - vertical: true, - children: notificationService.notifications.map((info) => { - const notification = Notification(info); - map.set(info.id, notification); - return notification; - }), - }); - - listBox = listBox - .hook(notificationService, (_, id: number) => map.delete(id), "closed") - .hook( - notificationService, - (_, id: number) => { - const info = notificationService.getNotification(id); - if (info === undefined) return; - const notification = Notification(info); - map.set(id, notification); - listBox.children = [notification, ...listBox.children]; - }, - "notified", - ); - return Widget.Scrollable({ className: "scrollable", visible: notifications.as((n) => n.length > 0), vexpand: true, hscroll: "never", - child: listBox, + child: Widget.Box({ + vertical: true, + children: notificationService.notifications.map((info) => { + const notification = Notification(info); + map.set(info.id, notification); + return notification; + }), + setup(self) { + self + .hook(notificationService, (_, id: number) => map.delete(id), "closed") + .hook( + notificationService, + (_, id: number) => { + const info = notificationService.getNotification(id); + if (info === undefined) return; + const notification = Notification(info); + map.set(id, notification); + self.children = [notification, ...self.children]; + }, + "notified", + ); + } + }), }); }; From ce1219ef1715ba0aab1cb9c479014888f6bb5f14 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Sun, 3 Nov 2024 10:08:19 +0100 Subject: [PATCH 05/21] fix(notification-menu): Inconsistent amount of notifications --- ags/timemenu/notification-list.ts | 65 +++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index 517e5e8..ce600e6 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -1,11 +1,14 @@ +import type { Variable as VariableType } from "types/variable"; + import Notification from "./notification"; const notificationService = await Service.import("notifications"); -const notifications = notificationService.bind("notifications"); -const Placeholder = () => +type NotificationMap = VariableType>>; + +const Placeholder = (notifications: NotificationMap) => Widget.Box({ - visible: notifications.as((n) => n.length === 0), + visible: notifications.bind().as((m) => m.size === 0), className: "placeholder", hexpand: true, vexpand: true, @@ -14,42 +17,62 @@ const Placeholder = () => children: [Widget.Icon("notifications-disabled-symbolic"), Widget.Label("Your inbox is empty")], }); -const NotificationList = () => { - // Keep track of registered notifications. - const map: Map> = new Map(); +const NotificationList = (notifications: NotificationMap) => { + const notify = (self: ReturnType, id: number | undefined) => { + if (id === undefined) return; + + const info = notificationService.getNotification(id); + if (info === undefined) return; + + const notification = Notification(info); + notifications.setValue(notifications.getValue().set(id, notification)); + self.children = [notification, ...self.children]; + }; + + const remove = (id: number | undefined) => { + if (id === undefined) return; + + const m = notifications.getValue(); + const notification = m.get(id); + if (notification === undefined) { + return; + } + + m.delete(id); + notification.destroy(); + notifications.setValue(m); + }; + return Widget.Scrollable({ className: "scrollable", - visible: notifications.as((n) => n.length > 0), + visible: notifications.bind().as((n) => n.size > 0), vexpand: true, hscroll: "never", child: Widget.Box({ vertical: true, children: notificationService.notifications.map((info) => { const notification = Notification(info); - map.set(info.id, notification); + notifications.setValue(notifications.getValue().set(info.id, notification)); return notification; }), setup(self) { self - .hook(notificationService, (_, id: number) => map.delete(id), "closed") .hook( notificationService, - (_, id: number) => { - const info = notificationService.getNotification(id); - if (info === undefined) return; - const notification = Notification(info); - map.set(id, notification); - self.children = [notification, ...self.children]; - }, + (_: unknown, id: number | undefined) => notify(self, id), "notified", - ); - } + ) + .hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed"); + }, }), }); }; -export default () => - Widget.Box({ +export default () => { + // Keep track of registered notifications. + const notifications: NotificationMap = Variable(new Map()); + return Widget.Box({ className: "list", - children: [Placeholder(), NotificationList()], + children: [Placeholder(notifications), NotificationList(notifications)], }); +}; From d5aa719d34e4b9aec6efd04f3951f5dd384210bd Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:38:42 +0100 Subject: [PATCH 06/21] fix(notification-menu): Remove notifications that override other notifications --- ags/timemenu/notification-list.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index ce600e6..b5a0b58 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -20,12 +20,22 @@ const Placeholder = (notifications: NotificationMap) => const NotificationList = (notifications: NotificationMap) => { const notify = (self: ReturnType, id: number | undefined) => { if (id === undefined) return; + const result = notifications.getValue(); + + // Remove notification with the same id. + // These are notifications that are meant to replace other notifications + // (e.g., currently playing song). + const replacedNotification = result.get(id); + if (replacedNotification !== undefined) { + result.delete(id); + replacedNotification.destroy(); + } const info = notificationService.getNotification(id); if (info === undefined) return; const notification = Notification(info); - notifications.setValue(notifications.getValue().set(id, notification)); + notifications.setValue(result.set(id, notification)); self.children = [notification, ...self.children]; }; @@ -62,7 +72,7 @@ const NotificationList = (notifications: NotificationMap) => { (_: unknown, id: number | undefined) => notify(self, id), "notified", ) - .hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed"); + .hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed") }, }), }); From 27a4c2d0458061ec4dd7448455df32e203d9e450 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Tue, 5 Nov 2024 20:47:30 +0100 Subject: [PATCH 07/21] style-wip(notification-menu): Start styling notification menu --- ags/style.scss | 12 ++++++++++++ ags/timemenu/notification-list.ts | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 230e43c..45d9cdc 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -394,4 +394,16 @@ window.darkened { min-height: 26em; border-radius: 1.5em; + + .placeholder { + + >.icon, + >label { + font-size: 1.22em; + } + + >.icon { + margin-right: 1em; + } + } } diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index b5a0b58..0293a67 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -14,7 +14,10 @@ const Placeholder = (notifications: NotificationMap) => vexpand: true, hpack: "center", vpack: "center", - children: [Widget.Icon("notifications-disabled-symbolic"), Widget.Label("Your inbox is empty")], + children: [ + Widget.Icon({ className: "icon", icon: "notifications-disabled-symbolic" }), + Widget.Label("Your inbox is empty"), + ], }); const NotificationList = (notifications: NotificationMap) => { @@ -72,7 +75,7 @@ const NotificationList = (notifications: NotificationMap) => { (_: unknown, id: number | undefined) => notify(self, id), "notified", ) - .hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed") + .hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed"); }, }), }); From ed28a87933afbd2eefac6756cc6ec8df5e068327 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:13:09 +0100 Subject: [PATCH 08/21] feat-wip(notification-menu): Start improving notification features --- ags/lib/format.ts | 9 ++++++ ags/lib/option-rs.ts | 53 +++++++++++++++++++++++++++++++ ags/style.scss | 2 ++ ags/timemenu/icon.ts | 38 ++++++++++++++++++++++ ags/timemenu/notification-list.ts | 2 -- ags/timemenu/notification.ts | 50 +++++++++++++++-------------- 6 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 ags/lib/format.ts create mode 100644 ags/lib/option-rs.ts create mode 100644 ags/timemenu/icon.ts diff --git a/ags/lib/format.ts b/ags/lib/format.ts new file mode 100644 index 0000000..fc0305b --- /dev/null +++ b/ags/lib/format.ts @@ -0,0 +1,9 @@ +import GLib from "gi://GLib"; + +export function formatTime(value: number, format: string = "%H:%M"): string { + const result = GLib.DateTime.new_from_unix_local(value).format(format); + if (result === null) { + throw new Error(`Failed to format time using \`${format}\` format.`); + } + return result; +} diff --git a/ags/lib/option-rs.ts b/ags/lib/option-rs.ts new file mode 100644 index 0000000..4d03486 --- /dev/null +++ b/ags/lib/option-rs.ts @@ -0,0 +1,53 @@ +export abstract class Option { + abstract orElse(fn: () => Option): Option; + abstract andThen(fn: (value: T) => Option): Option; + abstract unwrapOr(value: T): T; + + static from(value: T | null | undefined): Some | None { + return value !== null && value !== undefined ? new Some(value) : new None(); + } + + isSome(): boolean { + return this instanceof Some; + } + + isNone(): boolean { + return this instanceof None; + } +} + +export class Some extends Option { + constructor(public value: T) { + super(); + } + + orElse(_fn: () => Option): Option { + return this; + } + + andThen(fn: (value: T) => Option): Option { + return fn(this.value); + } + + unwrapOr(_value: T): T { + return this.value; + } +} + +export class None extends Option { + constructor() { + super(); + } + + orElse(fn: () => Option): Option { + return fn(); + } + + andThen(_fn: (value: T) => Option): Option { + return new None(); + } + + unwrapOr(value: T): T { + return value; + } +} diff --git a/ags/style.scss b/ags/style.scss index 45d9cdc..743c7dc 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -406,4 +406,6 @@ window.darkened { margin-right: 1em; } } + + scrollable {} } diff --git a/ags/timemenu/icon.ts b/ags/timemenu/icon.ts new file mode 100644 index 0000000..e0776dc --- /dev/null +++ b/ags/timemenu/icon.ts @@ -0,0 +1,38 @@ +import type { Notification as NotificationInfo } from "types/service/notifications"; + +import { Option, Some, None } from "lib/option-rs"; + +const iconAsOption = (icon: string): Option => { + return Utils.lookUpIcon(icon) ? new Some(icon) : new None(); +}; + +export default ({ app_entry, app_icon, image }: NotificationInfo) => { + if (image !== undefined) { + print("Image provided, using image."); + return Widget.Box({ + className: "icon", + css: ` + background-image: url("${image}"); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + `, + }); + } + + const icon = Option.from(app_entry) + .andThen(iconAsOption) + .orElse(() => iconAsOption(app_icon)) + .unwrapOr(""); // TODO: Add some fallback icon here.; + + return Widget.Box({ + className: "icon", + child: Widget.Icon({ + icon, + hpack: "center", + vpack: "center", + hexpand: true, + vexpand: true, + }), + }); +}; diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index 0293a67..3723928 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -57,7 +57,6 @@ const NotificationList = (notifications: NotificationMap) => { }; return Widget.Scrollable({ - className: "scrollable", visible: notifications.bind().as((n) => n.size > 0), vexpand: true, hscroll: "never", @@ -85,7 +84,6 @@ export default () => { // Keep track of registered notifications. const notifications: NotificationMap = Variable(new Map()); return Widget.Box({ - className: "list", children: [Placeholder(notifications), NotificationList(notifications)], }); }; diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 43c6252..c54398b 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -1,10 +1,14 @@ import type { Notification as NotificationInfo } from "types/service/notifications"; +import NotificationIcon from "./icon"; + +import { formatTime } from "lib/format"; + export default (notification: NotificationInfo) => { const content = Widget.Box({ class_name: "content", children: [ - // NotificationIcon(notification), + NotificationIcon(notification), Widget.Box({ hexpand: true, vertical: true, @@ -22,11 +26,11 @@ export default (notification: NotificationInfo) => { label: notification.summary.trim(), use_markup: true, }), - // Widget.Label({ - // class_name: "time", - // vpack: "start", - // label: time(notification.time), - // }), + Widget.Label({ + class_name: "time", + vpack: "start", + label: formatTime(notification.time), + }), Widget.Button({ class_name: "close-button", vpack: "start", @@ -70,25 +74,23 @@ export default (notification: NotificationInfo) => { }) : null; - const eventbox = Widget.EventBox({ - vexpand: false, - on_primary_click: notification.dismiss, - on_hover() { - if (actionsbox) actionsbox.reveal_child = true; - }, - on_hover_lost() { - if (actionsbox) actionsbox.reveal_child = true; - - notification.dismiss(); - }, - child: Widget.Box({ - vertical: true, - children: actionsbox ? [content, actionsbox] : [content], - }), - }); - return Widget.Box({ class_name: `notification ${notification.urgency}`, - child: eventbox, + child: Widget.EventBox({ + vexpand: false, + on_primary_click: notification.dismiss, + on_hover() { + if (actionsbox) actionsbox.reveal_child = true; + }, + on_hover_lost() { + if (actionsbox) actionsbox.reveal_child = true; + + notification.dismiss(); + }, + child: Widget.Box({ + vertical: true, + children: actionsbox ? [content, actionsbox] : [content], + }), + }), }); }; From 6eef9c1e7440050246dc7fd6574d7e768ca19065 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:12:30 +0100 Subject: [PATCH 09/21] style-wip(notification-menu): Continue styling notifications --- ags/style.scss | 30 +++++++++++++++++++++++++++++- ags/timemenu/icon.ts | 2 +- ags/timemenu/notification-list.ts | 1 + ags/timemenu/notification.ts | 31 ++++++++++++++++--------------- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 743c7dc..a0e7b28 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -407,5 +407,33 @@ window.darkened { } } - scrollable {} + .notifications { + @include space-between-y(0.75em); + + .notification>.event-box { + border-radius: 0.5em; + border: 1px solid scale-color($background1, $lightness: 7.5%); + background-color: $surface0; + + &:hover { + background-color: hover-color($surface0) + } + + .content { + padding: 0.5em; + + $icon-width: 4em; + + .icon { + margin-right: 1em; + min-width: $icon-width; + min-height: $icon-width; + + >.image { + font-size: $icon-width; + } + } + } + } + } } diff --git a/ags/timemenu/icon.ts b/ags/timemenu/icon.ts index e0776dc..5a18dee 100644 --- a/ags/timemenu/icon.ts +++ b/ags/timemenu/icon.ts @@ -8,7 +8,6 @@ const iconAsOption = (icon: string): Option => { export default ({ app_entry, app_icon, image }: NotificationInfo) => { if (image !== undefined) { - print("Image provided, using image."); return Widget.Box({ className: "icon", css: ` @@ -29,6 +28,7 @@ export default ({ app_entry, app_icon, image }: NotificationInfo) => { className: "icon", child: Widget.Icon({ icon, + className: "image", hpack: "center", vpack: "center", hexpand: true, diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index 3723928..e41d429 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -61,6 +61,7 @@ const NotificationList = (notifications: NotificationMap) => { vexpand: true, hscroll: "never", child: Widget.Box({ + className: "notifications", vertical: true, children: notificationService.notifications.map((info) => { const notification = Notification(info); diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index c54398b..a980648 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -57,26 +57,27 @@ export default (notification: NotificationInfo) => { const actionsbox = notification.actions.length > 0 ? Widget.Revealer({ - transition: "slide_down", - child: Widget.EventBox({ - child: Widget.Box({ - class_name: "actions horizontal", - children: notification.actions.map((action) => - Widget.Button({ - class_name: "action-button", - on_clicked: () => notification.invoke(action.id), - hexpand: true, - child: Widget.Label(action.label), - }), - ), - }), + transition: "slide_down", + child: Widget.EventBox({ + child: Widget.Box({ + class_name: "actions horizontal", + children: notification.actions.map((action) => + Widget.Button({ + class_name: "action-button", + on_clicked: () => notification.invoke(action.id), + hexpand: true, + child: Widget.Label(action.label), + }), + ), }), - }) + }), + }) : null; return Widget.Box({ - class_name: `notification ${notification.urgency}`, + className: `notification ${notification.urgency}`, child: Widget.EventBox({ + className: "event-box", vexpand: false, on_primary_click: notification.dismiss, on_hover() { From 0058b66ae4c13d33518c47f8c0175a2e27ca736e Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:30:06 +0100 Subject: [PATCH 10/21] feat(dev): Add hot reloading off css styles --- ags/config.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ags/config.js b/ags/config.js index 9c091d8..10f53e2 100644 --- a/ags/config.js +++ b/ags/config.js @@ -35,6 +35,25 @@ async function compileMain(dest) { await compileStyles(dest); await compileMain(dest); + // Set up hot reloading of styles. + Utils.monitorFile( + `${App.configDir}/style.scss`, + (_, event) => { + if (event !== 0) { + // Not a changed event. + return; + } + + print("Hot reloading styles."); + Promise.resolve(compileStyles(dest)).then((_) => { + App.resetCss() + App.applyCss(`${dest}/style.css`) + }).catch((reason) => { + print(`Hot reloading error: ${reason}`); + }); + } + ); + (await import(`file://${dest}/main.js`)).main(dest); } catch (err) { console.error(err); From 9bcd2de9739a435e3e6ec046d11aecba02784f8a Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:30:53 +0100 Subject: [PATCH 11/21] fix(notification-menu): Move around classnames to be more consistent --- ags/style.scss | 55 ++++++++++++++++--------------- ags/timemenu/icon.ts | 6 ++-- ags/timemenu/notification-list.ts | 3 +- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index a0e7b28..2818377 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -395,42 +395,45 @@ window.darkened { border-radius: 1.5em; - .placeholder { + .notifications { - >.icon, - >label { - font-size: 1.22em; - } + .placeholder { + + >.icon, + >label { + font-size: 1.22em; + } - >.icon { - margin-right: 1em; + >.icon { + margin-right: 1em; + } } - } - .notifications { - @include space-between-y(0.75em); + .list { + @include space-between-y(0.75em); - .notification>.event-box { - border-radius: 0.5em; - border: 1px solid scale-color($background1, $lightness: 7.5%); - background-color: $surface0; + .notification>.event-box { + border-radius: 0.5em; + border: 1px solid scale-color($background1, $lightness: 7.5%); + background-color: $surface0; - &:hover { - background-color: hover-color($surface0) - } + &:hover { + background-color: hover-color($surface0) + } - .content { - padding: 0.5em; + .content { + padding: 0.5em; - $icon-width: 4em; + .image { + $icon-width: 4em; - .icon { - margin-right: 1em; - min-width: $icon-width; - min-height: $icon-width; + margin-right: 1em; + min-width: $icon-width; + min-height: $icon-width; - >.image { - font-size: $icon-width; + >.icon { + font-size: $icon-width; + } } } } diff --git a/ags/timemenu/icon.ts b/ags/timemenu/icon.ts index 5a18dee..91473d9 100644 --- a/ags/timemenu/icon.ts +++ b/ags/timemenu/icon.ts @@ -9,7 +9,7 @@ const iconAsOption = (icon: string): Option => { export default ({ app_entry, app_icon, image }: NotificationInfo) => { if (image !== undefined) { return Widget.Box({ - className: "icon", + className: "image", css: ` background-image: url("${image}"); background-size: cover; @@ -25,10 +25,10 @@ export default ({ app_entry, app_icon, image }: NotificationInfo) => { .unwrapOr(""); // TODO: Add some fallback icon here.; return Widget.Box({ - className: "icon", + className: "image", child: Widget.Icon({ icon, - className: "image", + className: "icon", hpack: "center", vpack: "center", hexpand: true, diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index e41d429..f320cba 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -61,7 +61,7 @@ const NotificationList = (notifications: NotificationMap) => { vexpand: true, hscroll: "never", child: Widget.Box({ - className: "notifications", + className: "list", vertical: true, children: notificationService.notifications.map((info) => { const notification = Notification(info); @@ -85,6 +85,7 @@ export default () => { // Keep track of registered notifications. const notifications: NotificationMap = Variable(new Map()); return Widget.Box({ + className: "notifications", children: [Placeholder(notifications), NotificationList(notifications)], }); }; From 0a1732fb2437672e64d35be0adf5c53ff2c08902 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:20:24 +0100 Subject: [PATCH 12/21] feat-wip(notification-menu): Hide the close button when hovering --- ags/style.scss | 4 +++- ags/timemenu/notification.ts | 35 +++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 2818377..fcc92c6 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -416,9 +416,11 @@ window.darkened { border-radius: 0.5em; border: 1px solid scale-color($background1, $lightness: 7.5%); background-color: $surface0; + box-shadow: 0 0.1em 0.25em rgba(20, 20, 20, 0.25); &:hover { - background-color: hover-color($surface0) + box-shadow: inset 1px 1px 1px 1px $surface0; + background-color: hover-color($surface0); } .content { diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index a980648..4a71ffd 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -5,6 +5,8 @@ import NotificationIcon from "./icon"; import { formatTime } from "lib/format"; export default (notification: NotificationInfo) => { + const closeButtonVisible = Variable(false); + const content = Widget.Box({ class_name: "content", children: [ @@ -24,29 +26,30 @@ export default (notification: NotificationInfo) => { truncate: "end", wrap: true, label: notification.summary.trim(), - use_markup: true, + useMarkup: true, }), Widget.Label({ - class_name: "time", + className: "time", vpack: "start", label: formatTime(notification.time), }), Widget.Button({ - class_name: "close-button", + className: "close-button", + visible: closeButtonVisible.bind(), vpack: "start", child: Widget.Icon("window-close-symbolic"), - on_clicked: notification.close, + onClicked: notification.close, }), ], }), Widget.Label({ - class_name: "description", + className: "description", hexpand: true, - use_markup: true, + useMarkup: true, xalign: 0, justification: "left", label: notification.body.trim(), - max_width_chars: 24, + maxWidthChars: 24, wrap: true, }), ], @@ -60,11 +63,11 @@ export default (notification: NotificationInfo) => { transition: "slide_down", child: Widget.EventBox({ child: Widget.Box({ - class_name: "actions horizontal", + className: "actions horizontal", children: notification.actions.map((action) => Widget.Button({ - class_name: "action-button", - on_clicked: () => notification.invoke(action.id), + className: "action-button", + onClicked: () => notification.invoke(action.id), hexpand: true, child: Widget.Label(action.label), }), @@ -79,14 +82,14 @@ export default (notification: NotificationInfo) => { child: Widget.EventBox({ className: "event-box", vexpand: false, - on_primary_click: notification.dismiss, - on_hover() { + onPrimaryClick: notification.dismiss, + onHover() { if (actionsbox) actionsbox.reveal_child = true; + closeButtonVisible.setValue(true); }, - on_hover_lost() { - if (actionsbox) actionsbox.reveal_child = true; - - notification.dismiss(); + onHoverLost() { + if (actionsbox) actionsbox.reveal_child = false; + closeButtonVisible.setValue(false); }, child: Widget.Box({ vertical: true, From a4c5919908877e2b7ec3518274ea07a0dbfda5d7 Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:56:03 +0100 Subject: [PATCH 13/21] format: Run biome format --- ags/config.js | 29 ++++++++++++++--------------- ags/timemenu/notification.ts | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/ags/config.js b/ags/config.js index 10f53e2..26a4fd5 100644 --- a/ags/config.js +++ b/ags/config.js @@ -36,23 +36,22 @@ async function compileMain(dest) { await compileMain(dest); // Set up hot reloading of styles. - Utils.monitorFile( - `${App.configDir}/style.scss`, - (_, event) => { - if (event !== 0) { - // Not a changed event. - return; - } - - print("Hot reloading styles."); - Promise.resolve(compileStyles(dest)).then((_) => { - App.resetCss() - App.applyCss(`${dest}/style.css`) - }).catch((reason) => { + Utils.monitorFile(`${App.configDir}/style.scss`, (_, event) => { + if (event !== 0) { + // Not a changed event. + return; + } + + print("Hot reloading styles."); + Promise.resolve(compileStyles(dest)) + .then((_) => { + App.resetCss(); + App.applyCss(`${dest}/style.css`); + }) + .catch((reason) => { print(`Hot reloading error: ${reason}`); }); - } - ); + }); (await import(`file://${dest}/main.js`)).main(dest); } catch (err) { diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 4a71ffd..21774e2 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -60,21 +60,21 @@ export default (notification: NotificationInfo) => { const actionsbox = notification.actions.length > 0 ? Widget.Revealer({ - transition: "slide_down", - child: Widget.EventBox({ - child: Widget.Box({ - className: "actions horizontal", - children: notification.actions.map((action) => - Widget.Button({ - className: "action-button", - onClicked: () => notification.invoke(action.id), - hexpand: true, - child: Widget.Label(action.label), - }), - ), + transition: "slide_down", + child: Widget.EventBox({ + child: Widget.Box({ + className: "actions horizontal", + children: notification.actions.map((action) => + Widget.Button({ + className: "action-button", + onClicked: () => notification.invoke(action.id), + hexpand: true, + child: Widget.Label(action.label), + }), + ), + }), }), - }), - }) + }) : null; return Widget.Box({ From 36797f02025607a09405ff8b9f09bbcc9b75c49c Mon Sep 17 00:00:00 2001 From: exellentcoin26 <60435960+exellentcoin26@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:04:59 +0100 Subject: [PATCH 14/21] chore: Only enable css hot-reload in development --- ags/lib/develop.ts | 18 ++++++++++++++++++ ags/lib/format.ts | 2 +- ags/lib/option-rs.ts | 4 ---- ags/lib/settings.ts | 1 + ags/main.ts | 4 ++++ ags/timemenu/icon.ts | 2 +- ags/timemenu/timemenu.ts | 2 +- 7 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 ags/lib/develop.ts diff --git a/ags/lib/develop.ts b/ags/lib/develop.ts new file mode 100644 index 0000000..cb2c53b --- /dev/null +++ b/ags/lib/develop.ts @@ -0,0 +1,18 @@ +export function SetupCssHotReload(dest: string) { + Utils.monitorFile(`${App.configDir}/style.scss`, (_, event) => { + if (event !== 0) { + // Not a changed event. + return; + } + + print("Hot reloading styles."); + Promise.resolve(compileStyles(dest)) + .then((_) => { + App.resetCss(); + App.applyCss(`${dest}/style.css`); + }) + .catch((reason) => { + print(`Hot reloading error: ${reason}`); + }); + }); +} diff --git a/ags/lib/format.ts b/ags/lib/format.ts index fc0305b..1ebee72 100644 --- a/ags/lib/format.ts +++ b/ags/lib/format.ts @@ -1,6 +1,6 @@ import GLib from "gi://GLib"; -export function formatTime(value: number, format: string = "%H:%M"): string { +export function formatTime(value: number, format = "%H:%M"): string { const result = GLib.DateTime.new_from_unix_local(value).format(format); if (result === null) { throw new Error(`Failed to format time using \`${format}\` format.`); diff --git a/ags/lib/option-rs.ts b/ags/lib/option-rs.ts index 4d03486..4916d90 100644 --- a/ags/lib/option-rs.ts +++ b/ags/lib/option-rs.ts @@ -35,10 +35,6 @@ export class Some extends Option { } export class None extends Option { - constructor() { - super(); - } - orElse(fn: () => Option): Option { return fn(); } diff --git a/ags/lib/settings.ts b/ags/lib/settings.ts index 86c16ff..bf94d44 100644 --- a/ags/lib/settings.ts +++ b/ags/lib/settings.ts @@ -29,6 +29,7 @@ export let config = { enable: opt(true), }, }, + development: opt(false), }; /** diff --git a/ags/main.ts b/ags/main.ts index a8264f7..e2a654d 100644 --- a/ags/main.ts +++ b/ags/main.ts @@ -1,6 +1,7 @@ import Gdk from "gi://Gdk"; import type Gtk from "gi://Gtk?version=3.0"; +import { SetupCssHotReload } from "lib/develop"; import { VolumePopup } from "osd-popup/osd-popup.js"; import { Bar } from "./bar/bar.js"; import { config, readConfig } from "./lib/settings.js"; @@ -14,6 +15,9 @@ function forMonitors(widget: (monitor: number) => Gtk.Window) { export function main(dest: string): void { readConfig(); + + if (config.development) SetupCssHotReload(dest); + App.config({ style: `${dest}/style.css`, windows: () => { diff --git a/ags/timemenu/icon.ts b/ags/timemenu/icon.ts index 91473d9..cc99448 100644 --- a/ags/timemenu/icon.ts +++ b/ags/timemenu/icon.ts @@ -1,6 +1,6 @@ import type { Notification as NotificationInfo } from "types/service/notifications"; -import { Option, Some, None } from "lib/option-rs"; +import { None, Option, Some } from "lib/option-rs"; const iconAsOption = (icon: string): Option => { return Utils.lookUpIcon(icon) ? new Some(icon) : new None(); diff --git a/ags/timemenu/timemenu.ts b/ags/timemenu/timemenu.ts index a74bbc2..abe5d4c 100644 --- a/ags/timemenu/timemenu.ts +++ b/ags/timemenu/timemenu.ts @@ -1,5 +1,5 @@ -import NotificationList from "./notification-list"; import { PopupWindow } from "window"; +import NotificationList from "./notification-list"; export default () => PopupWindow({ From eb337f7e6d3f1f49b3c6c737d41a5406070feb32 Mon Sep 17 00:00:00 2001 From: andreashgk Date: Fri, 15 Nov 2024 14:34:17 +0100 Subject: [PATCH 15/21] refactor: make notification center wider --- ags/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index fcc92c6..f91687f 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -390,7 +390,7 @@ window.darkened { @include space-between-y(1.5em); padding: 1em; - min-width: 24em; + min-width: 28em; min-height: 26em; border-radius: 1.5em; @@ -441,4 +441,4 @@ window.darkened { } } } -} +} \ No newline at end of file From f87165395b615117a6517c27a5e9af94c5efa23a Mon Sep 17 00:00:00 2001 From: andreashgk Date: Fri, 15 Nov 2024 14:49:07 +0100 Subject: [PATCH 16/21] refactor: change placeholder styling --- ags/style.scss | 9 ++++----- ags/timemenu/notification-list.ts | 9 +++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index f91687f..275e962 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -398,14 +398,13 @@ window.darkened { .notifications { .placeholder { + @include space-between-y(1em); >.icon, >label { - font-size: 1.22em; - } - - >.icon { - margin-right: 1em; + color: mix($background1, $text, $weight: 25%); + font-size: 1.25em; + font-weight: bold; } } diff --git a/ags/timemenu/notification-list.ts b/ags/timemenu/notification-list.ts index f320cba..55fa270 100644 --- a/ags/timemenu/notification-list.ts +++ b/ags/timemenu/notification-list.ts @@ -12,11 +12,16 @@ const Placeholder = (notifications: NotificationMap) => className: "placeholder", hexpand: true, vexpand: true, + vertical: true, hpack: "center", vpack: "center", children: [ - Widget.Icon({ className: "icon", icon: "notifications-disabled-symbolic" }), - Widget.Label("Your inbox is empty"), + Widget.Icon({ + className: "icon", + icon: "org.gnome.Settings-notifications-symbolic", + size: 80, + }), + Widget.Label("No notifications"), ], }); From 7bea86dd590de34e24fd0ba51fd3254b6e9b530d Mon Sep 17 00:00:00 2001 From: andreashgk Date: Thu, 12 Dec 2024 12:28:04 +0100 Subject: [PATCH 17/21] refactor: make notification styling more like GNOME --- ags/style.scss | 82 ++++++++++++++++++++------ ags/theme.scss | 5 +- ags/timemenu/notification.ts | 108 +++++++++++++++++++++++------------ 3 files changed, 136 insertions(+), 59 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 275e962..622a72c 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -393,8 +393,6 @@ window.darkened { min-width: 28em; min-height: 26em; - border-radius: 1.5em; - .notifications { .placeholder { @@ -412,30 +410,76 @@ window.darkened { @include space-between-y(0.75em); .notification>.event-box { - border-radius: 0.5em; - border: 1px solid scale-color($background1, $lightness: 7.5%); - background-color: $surface0; - box-shadow: 0 0.1em 0.25em rgba(20, 20, 20, 0.25); + border-radius: 1em; + background-color: $background2; - &:hover { - box-shadow: inset 1px 1px 1px 1px $surface0; - background-color: hover-color($surface0); - } + .notification-inner { + padding: 1em; + @include space-between-y(0.25em); + + .header { + @include space-between-x(0.75em); - .content { - padding: 0.5em; + .app-icon { + color: mix($background2, $text, 15%); + font-weight: bold; + font-size: 13px; + } - .image { - $icon-width: 4em; + .app-name { + color: mix($background2, $text, 15%); + font-weight: bold; + font-size: 13px; + } - margin-right: 1em; - min-width: $icon-width; - min-height: $icon-width; + .time { + padding-top: 0.15em; + color: mix($background2, $text, 25%); + font-size: 12px; + } - >.icon { - font-size: $icon-width; + .close-button { + border-radius: 9999px; + background-color: $surface0; + padding: 0.3em; } } + + .content { + @include space-between-x(0.75em); + + .image { + $icon-width: 3.25em; + + min-width: $icon-width; + min-height: $icon-width; + + border-radius: 9999px; + + background-size: cover; + background-repeat: no-repeat; + background-position: center; + } + + .text { + @include space-between-y(0.4em); + + .title { + font-weight: bold; + } + + .description { + font-size: 14px; + } + } + } + } + + /* box-shadow: 0 0.1em 0.25em rgba(20, 20, 20, 0.25); */ + + &:hover { + /* box-shadow: inset 1px 1px 1px 1px $s; */ + /* background-color: hover-color($background2); */ } } } diff --git a/ags/theme.scss b/ags/theme.scss index e30c6f3..9e33125 100644 --- a/ags/theme.scss +++ b/ags/theme.scss @@ -5,6 +5,7 @@ $primary: #94e2d5; $text: #cdd6f4; -$background0: #181825; -$background1: #1e1e2e; +$background0: #11111b; +$background1: #181825; +$background2: #1e1e2e; $surface0: #313244; \ No newline at end of file diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 21774e2..519e410 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -3,56 +3,96 @@ import type { Notification as NotificationInfo } from "types/service/notificatio import NotificationIcon from "./icon"; import { formatTime } from "lib/format"; +import { conditionalChildren } from "lib/widgets"; export default (notification: NotificationInfo) => { - const closeButtonVisible = Variable(false); - const content = Widget.Box({ - class_name: "content", + className: "notification-inner", + vertical: true, children: [ - NotificationIcon(notification), Widget.Box({ + className: "header", hexpand: true, - vertical: true, - children: [ + vertical: false, + children: conditionalChildren([ + notification.app_icon + ? Widget.Icon({ + className: "app-icon", + vpack: "start", + icon: notification.app_icon, + size: 12, + }) + : null, + Widget.Label({ + className: "app-name", + vpack: "start", + label: notification.app_name, + }), + Widget.Label({ + className: "time", + vpack: "start", + label: formatTime(notification.time), + }), + Widget.Button({ + className: "close-button", + hexpand: true, + vpack: "center", + hpack: "end", + child: Widget.Icon("window-close-symbolic"), + onClicked: notification.close, + }), + ]), + }), + Widget.Box({ + class_name: "content", + vertical: false, + children: conditionalChildren([ + notification.image + ? Widget.Box({ + vertical: true, + children: [ + Widget.Box({ + className: "image", + vexpand: false, + css: `background-image: url("${notification.image}");`, + }), + // Needed so the icon does not get stretched vertically. + Widget.Box({ + vexpand: true, + }), + ], + }) + : null, Widget.Box({ + vertical: true, + hexpand: true, + hpack: "start", + className: "text", + vpack: "center", children: [ Widget.Label({ className: "title", - xalign: 0, + hpack: "start", justification: "left", - hexpand: true, - maxWidthChars: 24, + xalign: 0, truncate: "end", + hexpand: true, wrap: true, label: notification.summary.trim(), useMarkup: true, }), Widget.Label({ - className: "time", - vpack: "start", - label: formatTime(notification.time), - }), - Widget.Button({ - className: "close-button", - visible: closeButtonVisible.bind(), - vpack: "start", - child: Widget.Icon("window-close-symbolic"), - onClicked: notification.close, + className: "description", + justification: "left", + xalign: 0, + useMarkup: true, + hpack: "start", + label: notification.body.trim(), + wrap: true, }), ], }), - Widget.Label({ - className: "description", - hexpand: true, - useMarkup: true, - xalign: 0, - justification: "left", - label: notification.body.trim(), - maxWidthChars: 24, - wrap: true, - }), - ], + ]), }), ], }); @@ -83,14 +123,6 @@ export default (notification: NotificationInfo) => { className: "event-box", vexpand: false, onPrimaryClick: notification.dismiss, - onHover() { - if (actionsbox) actionsbox.reveal_child = true; - closeButtonVisible.setValue(true); - }, - onHoverLost() { - if (actionsbox) actionsbox.reveal_child = false; - closeButtonVisible.setValue(false); - }, child: Widget.Box({ vertical: true, children: actionsbox ? [content, actionsbox] : [content], From b99a10e9eb8f9e83872dd0dcd92e2d7ba07d9186 Mon Sep 17 00:00:00 2001 From: andreashgk Date: Thu, 12 Dec 2024 12:31:45 +0100 Subject: [PATCH 18/21] chore: remove unused icons file --- ags/timemenu/icon.ts | 38 ------------------------------------ ags/timemenu/notification.ts | 2 -- 2 files changed, 40 deletions(-) delete mode 100644 ags/timemenu/icon.ts diff --git a/ags/timemenu/icon.ts b/ags/timemenu/icon.ts deleted file mode 100644 index cc99448..0000000 --- a/ags/timemenu/icon.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Notification as NotificationInfo } from "types/service/notifications"; - -import { None, Option, Some } from "lib/option-rs"; - -const iconAsOption = (icon: string): Option => { - return Utils.lookUpIcon(icon) ? new Some(icon) : new None(); -}; - -export default ({ app_entry, app_icon, image }: NotificationInfo) => { - if (image !== undefined) { - return Widget.Box({ - className: "image", - css: ` - background-image: url("${image}"); - background-size: cover; - background-repeat: no-repeat; - background-position: center; - `, - }); - } - - const icon = Option.from(app_entry) - .andThen(iconAsOption) - .orElse(() => iconAsOption(app_icon)) - .unwrapOr(""); // TODO: Add some fallback icon here.; - - return Widget.Box({ - className: "image", - child: Widget.Icon({ - icon, - className: "icon", - hpack: "center", - vpack: "center", - hexpand: true, - vexpand: true, - }), - }); -}; diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 519e410..ca0ac57 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -1,7 +1,5 @@ import type { Notification as NotificationInfo } from "types/service/notifications"; -import NotificationIcon from "./icon"; - import { formatTime } from "lib/format"; import { conditionalChildren } from "lib/widgets"; From 84305e5dec5e77da24a5ad9647af2c28257e1bfa Mon Sep 17 00:00:00 2001 From: andreashgk Date: Thu, 12 Dec 2024 12:42:57 +0100 Subject: [PATCH 19/21] refactor: reduce notification text size --- ags/style.scss | 1 + ags/timemenu/notification.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ags/style.scss b/ags/style.scss index 622a72c..8ffd45a 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -466,6 +466,7 @@ window.darkened { .title { font-weight: bold; + font-size: 14px; } .description { diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index ca0ac57..4eb789a 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -71,6 +71,7 @@ export default (notification: NotificationInfo) => { Widget.Label({ className: "title", hpack: "start", + vpack: "end", justification: "left", xalign: 0, truncate: "end", @@ -85,6 +86,7 @@ export default (notification: NotificationInfo) => { xalign: 0, useMarkup: true, hpack: "start", + vpack: "end", label: notification.body.trim(), wrap: true, }), From 537abc01d03a814b73fe1ae4694e20588dec9d69 Mon Sep 17 00:00:00 2001 From: andreashgk Date: Thu, 12 Dec 2024 12:50:34 +0100 Subject: [PATCH 20/21] refactor: improve notification close button positioning --- ags/style.scss | 13 +++++++++---- ags/timemenu/notification.ts | 15 ++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 8ffd45a..932c874 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -438,10 +438,15 @@ window.darkened { font-size: 12px; } - .close-button { - border-radius: 9999px; - background-color: $surface0; - padding: 0.3em; + .buttons { + margin-top: -0.3em; + margin-right: -0.3em; + + .close-button { + border-radius: 9999px; + background-color: $surface0; + padding: 0.325em; + } } } diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index 4eb789a..e63f14d 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -31,13 +31,18 @@ export default (notification: NotificationInfo) => { vpack: "start", label: formatTime(notification.time), }), - Widget.Button({ - className: "close-button", - hexpand: true, + Widget.Box({ + className: "buttons", vpack: "center", hpack: "end", - child: Widget.Icon("window-close-symbolic"), - onClicked: notification.close, + hexpand: true, + children: [ + Widget.Button({ + className: "close-button", + child: Widget.Icon("window-close-symbolic"), + onClicked: notification.close, + }), + ], }), ]), }), From b5749ff3522c0844c634c0e544e3e1525edf0520 Mon Sep 17 00:00:00 2001 From: andreashgk Date: Thu, 12 Dec 2024 13:27:55 +0100 Subject: [PATCH 21/21] chore: remove unused code --- ags/timemenu/notification.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/ags/timemenu/notification.ts b/ags/timemenu/notification.ts index e63f14d..769a398 100644 --- a/ags/timemenu/notification.ts +++ b/ags/timemenu/notification.ts @@ -102,35 +102,14 @@ export default (notification: NotificationInfo) => { ], }); - const actionsbox = - notification.actions.length > 0 - ? Widget.Revealer({ - transition: "slide_down", - child: Widget.EventBox({ - child: Widget.Box({ - className: "actions horizontal", - children: notification.actions.map((action) => - Widget.Button({ - className: "action-button", - onClicked: () => notification.invoke(action.id), - hexpand: true, - child: Widget.Label(action.label), - }), - ), - }), - }), - }) - : null; - return Widget.Box({ className: `notification ${notification.urgency}`, child: Widget.EventBox({ className: "event-box", vexpand: false, - onPrimaryClick: notification.dismiss, child: Widget.Box({ vertical: true, - children: actionsbox ? [content, actionsbox] : [content], + children: [content], }), }), });