diff options
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/Drawer.qml | 58 | ||||
| -rw-r--r-- | desktop/NotificationHistory.qml | 70 | ||||
| -rw-r--r-- | desktop/NotificationList.qml | 90 | ||||
| -rw-r--r-- | desktop/NotificationRow.qml | 126 | ||||
| l--------- | desktop/Notify.qml | 1 | ||||
| -rw-r--r-- | desktop/modules/status/SnoozeRow.qml | 114 | ||||
| -rw-r--r-- | desktop/modules/status/StatusPage.qml | 10 |
7 files changed, 460 insertions, 9 deletions
diff --git a/desktop/Drawer.qml b/desktop/Drawer.qml index 4ed37d3..c02a4ba 100644 --- a/desktop/Drawer.qml +++ b/desktop/Drawer.qml @@ -10,6 +10,7 @@ // GNU General Public License for more details. import Quickshell +import Quickshell.Io import Quickshell.Wayland import QtQuick @@ -29,6 +30,25 @@ Scope { // Which module's page is showing. Empty means the grid. property string page: "" + // Set by NotificationList's History button and read by the history view + // loader below; reset when the drawer closes. + property bool history: false + + // The balloons read this to stand down while the drawer holds the space. + FileView { + id: drawerFlag + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/notifyd/drawer" + atomicWrites: true + printErrors: false + } + + onOpenChanged: drawerFlag.setText(root.open ? "1\n" : "0\n") + + // onOpenChanged does not fire for the initial value, so a restart while + // the drawer was open would leave the flag stale and suppress every + // balloon. Publish the starting state explicitly. + Component.onCompleted: drawerFlag.setText(root.open ? "1\n" : "0\n") + readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] @@ -41,6 +61,9 @@ Scope { } function show(name) { + // Deep-linking to a page must leave the history view, or both loaders + // stay active and the later-declared history loader paints on top. + root.history = false; root.page = root.modules.some(m => m.name === name) ? name : ""; root.open = true; } @@ -50,6 +73,7 @@ Scope { // Reset to the grid: a panel that reopens somewhere unexpected is // worse than one extra click. root.page = ""; + root.history = false; } function toggle(name) { @@ -109,7 +133,8 @@ Scope { // to it. focus: true Keys.onEscapePressed: { - if (root.page) root.page = ""; + if (root.history) root.history = false; + else if (root.page) root.page = ""; else root.close(); } @@ -121,18 +146,17 @@ Scope { Item { anchors.fill: parent anchors.margins: 16 - visible: root.page === "" - - // Reserved for the notification engine. An empty Item that - // claims the space rather than a placeholder graphic: the - // grid has to sit where it will sit once notifications - // arrive, or the layout is tuned against a position that - // does not survive. - Item { + visible: root.page === "" && !root.history + + // The notification engine's space. The live queue lists + // here while the drawer is open; the balloons stand down + // because the drawer flag above is set. + NotificationList { id: notifications anchors { top: parent.top; left: parent.left; right: parent.right } anchors.bottom: grid.top anchors.bottomMargin: 16 + onHistory: root.history = true } // Fixed, never scrolled. Three columns at 600px with a @@ -196,6 +220,22 @@ Scope { } Behavior on opacity { NumberAnimation { duration: 160 } } } + + // --- history view --- + + Loader { + id: historyLoader + anchors.fill: parent + anchors.margins: 16 + active: root.history + sourceComponent: Component { + Page { + title: "History" + NotificationHistory { width: parent.width } + } + } + onLoaded: if (item && item.back) item.back.connect(() => root.history = false) + } } } } diff --git a/desktop/NotificationHistory.qml b/desktop/NotificationHistory.qml new file mode 100644 index 0000000..8499192 --- /dev/null +++ b/desktop/NotificationHistory.qml @@ -0,0 +1,70 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick + +// The history ring: the last 20 dismissed or evicted notifications, newest +// first, with a clear all. Rows are inert; only clear-all removes them, since +// the daemon has no per-id history deletion. +Column { + id: page + + spacing: 8 + + Item { + width: parent.width + height: 28 + + Text { + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + text: Notify.history.length + " in history" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + } + + Text { + id: clearBtn + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + visible: Notify.history.length > 0 + text: "Clear all" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: clearArea.containsMouse ? Theme.red : Theme.subtext + + MouseArea { + id: clearArea + anchors.fill: parent + anchors.margins: -6 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Notify.clearHistory() + } + } + } + + Repeater { + model: Notify.history + + NotificationRow { + required property var modelData + width: page.width + notification: modelData + live: false + } + } + + Text { + width: parent.width + visible: Notify.history.length === 0 + text: "Nothing in history" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } +} diff --git a/desktop/NotificationList.qml b/desktop/NotificationList.qml new file mode 100644 index 0000000..9fe4328 --- /dev/null +++ b/desktop/NotificationList.qml @@ -0,0 +1,90 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick + +// The drawer's reserved notification space: a header with a History button, +// then the live queue, scrollable because the queue can hold 20 and the grid +// below is fixed. The grid owns its own position, so this space only fills +// what the grid leaves. +Item { + id: list + + signal history + + Column { + anchors.fill: parent + spacing: 6 + + Item { + width: parent.width + height: 28 + + Text { + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + text: "Notifications" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } + color: Theme.subtext + } + + Text { + id: historyBtn + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + text: "History" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: historyArea.containsMouse ? Theme.accent : Theme.subtext + + MouseArea { + id: historyArea + anchors.fill: parent + anchors.margins: -6 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: list.history() + } + } + } + + Flickable { + id: flick + width: parent.width + height: parent.height - 34 + clip: true + contentWidth: width + contentHeight: col.implicitHeight + boundsBehavior: Flickable.StopAtBounds + + Column { + id: col + width: flick.width + spacing: 8 + + Repeater { + model: Notify.queue + NotificationRow { + required property var modelData + width: col.width + notification: modelData + live: true + } + } + + Text { + width: col.width + visible: Notify.queue.length === 0 + text: "No notifications" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + } + } +} diff --git a/desktop/NotificationRow.qml b/desktop/NotificationRow.qml new file mode 100644 index 0000000..923cc9e --- /dev/null +++ b/desktop/NotificationRow.qml @@ -0,0 +1,126 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick + +// One notification in the drawer: app, summary and body. Live rows close with +// the X and click through to their actions; a history row is inert. +Rectangle { + id: row + + radius: 8 + color: Qt.alpha(Theme.surface, 0.6) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.08) + + required property var notification + // A history row has no live client: no X, and a click does nothing. + property bool live: true + + readonly property bool inert: row.notification.expires !== 0 && Date.now() >= row.notification.expires + + implicitHeight: texts.implicitHeight + 24 + + // "Sep 15 14:32" from the notification's created epoch-ms, local time. No + // year: the history ring is only 20 entries, so everything here is recent. + function stamp(ms) { + const d = new Date(ms); + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + function pad(n) { return (n < 10 ? "0" : "") + n; } + return months[d.getMonth()] + " " + d.getDate() + " " + + pad(d.getHours()) + ":" + pad(d.getMinutes()); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + Notify.closeAll(); + return; + } + if (!row.live) return; + const acts = row.notification.actions || []; + if (acts.length > 0 && !row.inert) Notify.actions(row.notification.id, acts); + else Notify.close(row.notification.id); + } + } + + Column { + id: texts + anchors { + left: parent.left + leftMargin: 12 + right: closeBtn.visible ? closeBtn.left : parent.right + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + spacing: 2 + + Row { + id: head + width: parent.width + spacing: 8 + + Text { + width: head.width - (stamp.visible ? stamp.implicitWidth + head.spacing : 0) + text: (row.notification.app || "") + (row.notification.summary ? " " + row.notification.summary : "") + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 2 } + color: Theme.text + } + + Text { + id: stamp + visible: row.notification.created > 0 + text: visible ? row.stamp(row.notification.created) : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + Text { + width: parent.width + visible: text !== "" + text: Notify.sanitize(row.notification.body) + textFormat: Text.RichText + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize } + color: Theme.subtext + } + } + + Text { + id: closeBtn + visible: row.live + anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } + width: 20 + height: 20 + text: "\uf00d" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font { family: Theme.iconFamily; pixelSize: 13 } + color: closeArea.containsMouse ? Theme.red : Theme.subtext + + MouseArea { + id: closeArea + anchors.fill: parent + anchors.margins: -6 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Notify.close(row.notification.id) + } + } +} diff --git a/desktop/Notify.qml b/desktop/Notify.qml new file mode 120000 index 0000000..635f31d --- /dev/null +++ b/desktop/Notify.qml @@ -0,0 +1 @@ +../shared/Notify.qml
\ No newline at end of file diff --git a/desktop/modules/status/SnoozeRow.qml b/desktop/modules/status/SnoozeRow.qml new file mode 100644 index 0000000..046d7cb --- /dev/null +++ b/desktop/modules/status/SnoozeRow.qml @@ -0,0 +1,114 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Controls +import "../.." + +// A switch plus a minutes field. On snoozes for the typed minutes through +// notify-snooze.sh; off clears. The last used value is persisted by the +// script, so the field prefills from it. +Item { + id: row + + // A tick so the switch reflects the snooze ending on its own. + property double now: Date.now() + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: row.now = Date.now() + } + + readonly property bool active: Notify.snoozeUntil > row.now + + implicitHeight: Math.max(texts.implicitHeight, sw.implicitHeight) + 16 + + function apply(on) { + if (on) { + const n = parseInt(minutes.text, 10); + snooze.command = ["notify-snooze.sh", String(isNaN(n) || n < 1 ? 30 : n)]; + } else { + snooze.command = ["notify-snooze.sh", "off"]; + } + snooze.running = false; + snooze.running = true; + } + + Process { id: snooze } + + FileView { + id: lastUsed + path: `${Quickshell.env("HOME")}/.local/state/notify-snooze.minutes` + printErrors: false + onLoaded: minutes.text = text().trim() + onLoadFailed: minutes.text = "30" + } + + Column { + id: texts + anchors { + left: parent.left + right: controls.left + rightMargin: 12 + verticalCenter: parent.verticalCenter + } + spacing: 2 + + Text { + text: "Snooze" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.text + } + + Text { + width: parent.width + wrapMode: Text.WordWrap + text: row.active ? "All notification balloons are held until snooze ends." + : "Hold every notification balloon for a number of minutes." + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + } + } + + Row { + id: controls + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 8 + + TextField { + id: minutes + width: 48 + height: 28 + text: "30" + horizontalAlignment: TextInput.AlignHCenter + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + background: Rectangle { + radius: 6 + color: Qt.alpha(Theme.surface, 0.9) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.15) + } + validator: IntValidator { bottom: 1; top: 1440 } + } + + Switch { + id: sw + checked: row.active + onToggled: row.apply(!row.active) + } + } + + onNowChanged: sw.checked = row.active +} diff --git a/desktop/modules/status/StatusPage.qml b/desktop/modules/status/StatusPage.qml index 850cc73..2fbdc9b 100644 --- a/desktop/modules/status/StatusPage.qml +++ b/desktop/modules/status/StatusPage.qml @@ -40,4 +40,14 @@ Column { description: "Do not disturb, no screen lock, breaktimer paused." value: Status.presentation } + + Rectangle { + width: page.width + height: 1 + color: Qt.alpha(Theme.text, 0.08) + } + + SnoozeRow { + width: page.width + } } |
