summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2025-09-13 14:56:55 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2025-09-13 14:56:55 +0200
commit3ce3fe19b11e30fd98d7eee06d56b90ae33b228d (patch)
tree572e904f871cd05410c21b33ddf9e0ae7e1c5c90
parent666464567055a2e4ba9f6bb310e901cdc27977f7 (diff)
downloadnixos-3ce3fe19b11e30fd98d7eee06d56b90ae33b228d.tar
nixos-3ce3fe19b11e30fd98d7eee06d56b90ae33b228d.tar.gz
nixos-3ce3fe19b11e30fd98d7eee06d56b90ae33b228d.tar.bz2
nixos-3ce3fe19b11e30fd98d7eee06d56b90ae33b228d.tar.xz
nixos-3ce3fe19b11e30fd98d7eee06d56b90ae33b228d.zip
...
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml4
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml40
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml213
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml51
4 files changed, 301 insertions, 7 deletions
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index dd0feb4b..426ed78c 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -66,9 +66,9 @@ PanelWindow {
66 anchors.verticalCenter: parent.verticalCenter 66 anchors.verticalCenter: parent.verticalCenter
67 spacing: 0 67 spacing: 0
68 68
69 WorktimeWidget { command: "time"; } 69 // WorktimeWidget { command: "time"; }
70 70
71 WorktimeWidget { command: "today"; } 71 // WorktimeWidget { command: "today"; }
72 72
73 KeyboardLayout {} 73 KeyboardLayout {}
74 74
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
index 589c36e5..a727b044 100644
--- a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
@@ -15,7 +15,7 @@ Scope {
15 id: notifsRepeater 15 id: notifsRepeater
16 16
17 model: ScriptModel { 17 model: ScriptModel {
18 values: NotificationManager.displayInhibited ? [] : [...NotificationManager.groups] 18 values: NotificationManager.displayInhibited ? [] : NotificationManager.groups
19 } 19 }
20 20
21 delegate: PanelWindow { 21 delegate: PanelWindow {
@@ -48,6 +48,7 @@ Scope {
48 readonly property real spaceAbove: { 48 readonly property real spaceAbove: {
49 var res = 0; 49 var res = 0;
50 for (let i = 0; i < notifWindow.index; i++) { 50 for (let i = 0; i < notifWindow.index; i++) {
51 (_ => {})(notifsRepeater.objectAt(i).modelData);
51 res += notifsRepeater.objectAt(i).height + 8; 52 res += notifsRepeater.objectAt(i).height + 8;
52 } 53 }
53 return res; 54 return res;
@@ -60,7 +61,7 @@ Scope {
60 61
61 color: "transparent" 62 color: "transparent"
62 63
63 implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + 16 64 implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16
64 implicitWidth: 400 65 implicitWidth: 400
65 66
66 WrapperMouseArea { 67 WrapperMouseArea {
@@ -108,6 +109,8 @@ Scope {
108 res += 1; 109 res += 1;
109 if (notifActions.visible) 110 if (notifActions.visible)
110 res += 1; 111 res += 1;
112 if (notifTime.visible)
113 res += 1;
111 return res; 114 return res;
112 } 115 }
113 116
@@ -167,7 +170,7 @@ Scope {
167 Layout.column: 0 170 Layout.column: 0
168 Layout.row: 0 171 Layout.row: 0
169 Layout.fillHeight: true 172 Layout.fillHeight: true
170 Layout.rowSpan: notifBody.visible ? 2 : 1 173 Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0)
171 } 174 }
172 175
173 Text { 176 Text {
@@ -176,6 +179,7 @@ Scope {
176 visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false 179 visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false
177 text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" 180 text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? ""
178 textFormat: Text.RichText 181 textFormat: Text.RichText
182 wrapMode: Text.Wrap
179 183
180 font.pointSize: 10 184 font.pointSize: 10
181 font.family: "Fira Sans" 185 font.family: "Fira Sans"
@@ -187,6 +191,32 @@ Scope {
187 Layout.columnSpan: notifCount.visible ? 2 : 1 191 Layout.columnSpan: notifCount.visible ? 2 : 1
188 } 192 }
189 193
194 Text {
195 id: notifTime
196
197 Connections {
198 target: NotificationManager.clock
199 function onDateChanged() {
200 notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime);
201 }
202 }
203
204 visible: notifTime.text && notifTime.text !== "now"
205 text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime)
206
207 font.pointSize: 8
208 font.family: "Fira Sans"
209 font.italic: true
210 color: "#555"
211 maximumLineCount: 1
212 horizontalAlignment: Text.AlignRight
213
214 Layout.fillWidth: true
215 Layout.row: notifBody.visible ? 2 : 1
216 Layout.column: notifImage.visible ? 1 : 0
217 Layout.columnSpan: 2
218 }
219
190 RowLayout { 220 RowLayout {
191 id: notifActions 221 id: notifActions
192 222
@@ -196,9 +226,9 @@ Scope {
196 uniformCellSizes: true 226 uniformCellSizes: true
197 227
198 width: 400 - 16 228 width: 400 - 16
199 Layout.row: notifBody.visible ? 2 : 1 229 Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0)
200 Layout.column: 0 230 Layout.column: 0
201 Layout.columnSpan: notifImage.visible ? 3 : 2 231 Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0)
202 232
203 Repeater { 233 Repeater {
204 model: ScriptModel { 234 model: ScriptModel {
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
index 3dadbc69..1005182a 100644
--- a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
@@ -2,6 +2,9 @@ import Quickshell
2import QtQuick 2import QtQuick
3import Quickshell.Widgets 3import Quickshell.Widgets
4import qs.Services 4import qs.Services
5import QtQuick.Controls
6import QtQuick.Layouts
7import QtQuick.Shapes
5 8
6Item { 9Item {
7 id: root 10 id: root
@@ -44,4 +47,214 @@ Item {
44 } 47 }
45 } 48 }
46 } 49 }
50
51 Loader {
52 id: tooltipLoader
53
54 active: false
55
56 Connections {
57 target: widgetMouseArea
58 function onContainsMouseChanged() {
59 if (widgetMouseArea.containsMouse)
60 tooltipLoader.active = true;
61 }
62 }
63
64 sourceComponent: PopupWindow {
65 id: tooltip
66
67 property bool nextVisible: !NotificationManager.displayInhibited && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse)
68
69 anchor {
70 item: widgetMouseArea
71 edges: Edges.Bottom | Edges.Left
72 }
73 visible: false
74
75 onNextVisibleChanged: hangTimer.restart()
76
77 Timer {
78 id: hangTimer
79 interval: tooltip.visible ? 100 : 500
80 onTriggered: {
81 tooltip.visible = tooltip.nextVisible;
82 if (!tooltip.visible)
83 tooltipLoader.active = false;
84 }
85 }
86
87 implicitWidth: 400
88 implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16))
89 color: "black"
90
91 WrapperMouseArea {
92 id: tooltipMouseArea
93
94 hoverEnabled: true
95 enabled: true
96
97 anchors.fill: parent
98
99 WrapperItem {
100 margin: 8
101
102 ScrollView {
103 id: scroll
104
105 contentWidth: availableWidth
106 // ScrollBar.vertical.policy: ScrollBar.AlwaysOn
107
108 ColumnLayout {
109 id: historyLayout
110 anchors {
111 left: parent.left
112 right: parent.right
113 }
114
115 spacing: 8
116
117 Repeater {
118 model: ScriptModel {
119 values: [...NotificationManager.history].reverse().map(o => o.notification)
120 }
121
122 delegate: GridLayout {
123 id: notif
124
125 Layout.fillWidth: true
126 Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8
127
128 required property var modelData
129 required property int index
130
131 columnSpacing: 8
132 rowSpacing: 8
133
134 columns: notifImage.visible ? 2 : 1
135 rows: {
136 var res = 2;
137 if (notifBody.visible)
138 res += 1;
139 if (notifSep.visible)
140 res += 1;
141 return res;
142 }
143
144 Shape {
145 id: notifSep
146
147 visible: notif.index != 0
148
149 height: 2
150 width: 400 - 32
151
152 ShapePath {
153 strokeWidth: 2
154 strokeColor: "#20ffffff"
155 startX: 0; startY: 0;
156 PathLine { x: 400 - 32; y: 0; }
157 }
158
159 Layout.row: 0
160 Layout.column: 0
161 Layout.columnSpan: notifImage.visible ? 2 : 1
162 Layout.alignment: Qt.AlignHCenter
163 }
164
165 Text {
166 id: notifSummary
167
168 text: notif.modelData?.summary ?? ""
169
170 font.pointSize: 10
171 font.family: "Fira Sans"
172 font.italic: true
173 color: "white"
174 maximumLineCount: 1
175 elide: Text.ElideRight
176
177 Layout.fillWidth: true
178 Layout.row: notifSep.visible ? 1 : 0
179 Layout.column: notifImage.visible ? 1 : 0
180 }
181
182 Image {
183 id: notifImage
184
185 visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false
186
187 onStatusChanged: {
188 if (notifImage.status == Image.Error)
189 notifImage.visible = false;
190 }
191
192 source: (notif.modelData?.image || notif.modelData?.appIcon) ?? ""
193 fillMode: Image.PreserveAspectFit
194 asynchronous: true
195 smooth: true
196 mipmap: true
197
198 Layout.maximumWidth: 50
199 Layout.column: 0
200 Layout.row: notifSep.visible ? 1 : 0
201 Layout.fillHeight: true
202 Layout.rowSpan: notifBody.visible ? 3 : 2
203 }
204
205 Text {
206 id: notifBody
207
208 visible: notif.modelData?.body ?? false
209 text: notif.modelData?.body ?? ""
210 textFormat: Text.RichText
211 wrapMode: Text.Wrap
212
213 font.pointSize: 10
214 font.family: "Fira Sans"
215 color: "white"
216
217 Layout.fillWidth: true
218 Layout.row: notifSep.visible ? 2 : 1
219 Layout.column: notifImage.visible ? 1 : 0
220 }
221
222 Text {
223 id: notifTime
224
225 Connections {
226 target: NotificationManager.clock
227 function onDateChanged() {
228 notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime);
229 }
230 }
231
232 text: NotificationManager.formatTime(notif.modelData?.receivedTime)
233
234 font.pointSize: 8
235 font.family: "Fira Sans"
236 font.italic: true
237 color: "#555"
238 maximumLineCount: 1
239 horizontalAlignment: Text.AlignRight
240
241 Layout.fillWidth: true
242 Layout.row: {
243 var res = 1;
244 if (notifSep.visible)
245 res += 1;
246 if (notifBody.visible)
247 res += 1;
248 return res;
249 }
250 Layout.column: notifImage.visible ? 1 : 0
251 }
252 }
253 }
254 }
255 }
256 }
257 }
258 }
259 }
47} 260}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
index 778cdc2a..2199ccdf 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
@@ -56,6 +56,8 @@ Singleton {
56 { "test": { "appName": "Element" }, "group-by": [ "summary" ] } 56 { "test": { "appName": "Element" }, "group-by": [ "summary" ] }
57 ]; 57 ];
58 58
59 property var history: []
60
59 Component { 61 Component {
60 id: expirationTimer 62 id: expirationTimer
61 63
@@ -74,6 +76,45 @@ Singleton {
74 } 76 }
75 } 77 }
76 78
79 Component {
80 id: notificationLock
81
82 RetainableLock {}
83 }
84
85 readonly property SystemClock clock: SystemClock {
86 precision: SystemClock.Minutes
87 }
88
89 function formatTime(time) {
90 const now = root.clock.date;
91 const diff = now - time;
92 const minutes = Math.ceil(diff / 60000);
93 const hours = Math.floor(minutes / 60);
94
95 if (hours < 1) {
96 if (minutes < 1)
97 return "now";
98 if (minutes == 1)
99 return "1 minute";
100 return `${minutes} minutes`;
101 }
102
103 const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
104 const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
105 const days = Math.floor((nowDate - timeDate) / (1000 * 86400))
106
107 const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm");
108
109 if (days === 0)
110 return timeStr;
111 if (days === 1)
112 return `yesterday ${timeStr}`;
113
114 const dateStr = time.toLocaleTimeString(Qt.locale(), "YYYY-MM-DD");
115 return `${dateStr} ${timeStr}`;
116 }
117
77 NotificationServer { 118 NotificationServer {
78 id: server 119 id: server
79 120
@@ -92,7 +133,17 @@ Singleton {
92 Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); 133 Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true });
93 notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); 134 notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout });
94 } 135 }
136 Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true });
137 notification.receivedTime = root.clock.date;
138 notification.closed.connect((reason) => server.onNotificationClosed(notification, reason));
95 notification.tracked = true; 139 notification.tracked = true;
96 } 140 }
141
142 function onNotificationClosed(notification, reason) {
143 root.history.push({
144 lock: notificationLock.createObject(root, { locked: true, object: notification }),
145 notification: notification
146 });
147 }
97 } 148 }
98} 149}