Compare commits

...
Author SHA1 Message Date
Damocles
d20fdf8fa0 move NotifService and NotifItem to services/, keep notification UI in modules/ 2026-04-18 00:26:14 +02:00
Damocles
a17a365b81 add notification icon pills to lock screen 2026-04-18 00:21:40 +02:00
11 changed files with 107 additions and 34 deletions

View file

@ -108,6 +108,7 @@ programs.nova-shell.modules = {
power.enable = false; # if you enjoy living dangerously without a logout button power.enable = false; # if you enjoy living dangerously without a logout button
lock.enable = false; # if you prefer your session unlocked and your secrets free lock.enable = false; # if you prefer your session unlocked and your secrets free
lock.screenshot = false; # disable blurred desktop screenshot background lock.screenshot = false; # disable blurred desktop screenshot background
lock.notifications = false; # hide notification icons on the lock screen
lock.mpris = false; # hide media controls on the lock screen lock.mpris = false; # hide media controls on the lock screen
lock.volume = false; # hide volume slider on the lock screen lock.volume = false; # hide volume slider on the lock screen

View file

@ -106,6 +106,11 @@ in
default = true; default = true;
description = "Show blurred desktop screenshot as lock screen background."; description = "Show blurred desktop screenshot as lock screen background.";
}; };
notifications = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Show notification app icons on the lock screen.";
};
mpris = lib.mkOption { mpris = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;

View file

@ -141,6 +141,74 @@ WlSessionLockSurface {
} }
} }
// Notification pills
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0
readonly property var _notifGroups: {
const notifs = S.NotifService.list.filter(n => n.state !== "dismissed");
const groups = {};
for (const n of notifs) {
const key = n.appIcon || n.appName || "unknown";
if (!groups[key])
groups[key] = {
icon: n.appIcon,
name: n.appName,
count: 0
};
groups[key].count++;
}
return Object.values(groups);
}
Repeater {
model: parent._notifGroups
delegate: Rectangle {
required property var modelData
width: _pillRow.implicitWidth + 12
height: 24
radius: 12
color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7)
border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3)
border.width: 1
Row {
id: _pillRow
anchors.centerIn: parent
spacing: 4
Image {
anchors.verticalCenter: parent.verticalCenter
width: 14
height: 14
source: {
const icon = modelData.icon;
if (!icon)
return "";
if (icon.startsWith("/"))
return icon;
return Quickshell.iconPath(icon) ?? "";
}
sourceSize: Qt.size(14, 14)
visible: source !== ""
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: modelData.count > 1 ? modelData.count.toString() : ""
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
visible: modelData.count > 1
}
}
}
}
}
// Spacer // Spacer
Item { Item {
width: 1 width: 1

View file

@ -33,8 +33,8 @@ M.HoverPanel {
// DND toggle // DND toggle
Text { Text {
text: M.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C" text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
color: M.NotifService.dnd ? S.Theme.base09 : S.Theme.base04 color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -42,7 +42,7 @@ M.HoverPanel {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: M.NotifService.toggleDnd() onClicked: S.NotifService.toggleDnd()
} }
} }
@ -53,7 +53,7 @@ M.HoverPanel {
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: M.NotifService.count > 0 visible: S.NotifService.count > 0
MouseArea { MouseArea {
id: clearArea id: clearArea
@ -88,7 +88,7 @@ M.HoverPanel {
// Group notifications by appName, sorted by max urgency desc then most recent time desc // Group notifications by appName, sorted by max urgency desc then most recent time desc
readonly property var _groups: { readonly property var _groups: {
const map = {}; const map = {};
for (const n of M.NotifService.list) { for (const n of S.NotifService.list) {
const key = n.appName || ""; const key = n.appName || "";
if (!map[key]) if (!map[key])
map[key] = { map[key] = {
@ -167,14 +167,14 @@ M.HoverPanel {
} }
function _cascadeDismiss() { function _cascadeDismiss() {
if (M.NotifService.list.length === 0) if (S.NotifService.list.length === 0)
return; return;
const ids = M.NotifService.list.map(n => n.id); const ids = S.NotifService.list.map(n => n.id);
_startCascade(_getVisibleNotifDelegates(), ids); _startCascade(_getVisibleNotifDelegates(), ids);
} }
function _cascadeGroupDismiss(appName) { function _cascadeGroupDismiss(appName) {
const ids = M.NotifService.list.filter(n => n.appName === appName).map(n => n.id); const ids = S.NotifService.list.filter(n => n.appName === appName).map(n => n.id);
if (ids.length === 0) if (ids.length === 0)
return; return;
_startCascade(_getVisibleNotifDelegates(appName), ids); _startCascade(_getVisibleNotifDelegates(appName), ids);
@ -184,7 +184,7 @@ M.HoverPanel {
const ids = _pendingDismissIds; const ids = _pendingDismissIds;
_pendingDismissIds = []; _pendingDismissIds = [];
for (const id of ids) for (const id of ids)
M.NotifService.dismiss(id); S.NotifService.dismiss(id);
} }
property Component _cascadeTimer: Component { property Component _cascadeTimer: Component {
@ -467,7 +467,7 @@ M.HoverPanel {
ScriptAction { ScriptAction {
script: { script: {
if (notifDelegate._notif && !notifDelegate._skipDismiss) if (notifDelegate._notif && !notifDelegate._skipDismiss)
M.NotifService.dismiss(notifDelegate._notif.id); S.NotifService.dismiss(notifDelegate._notif.id);
} }
} }
} }
@ -476,7 +476,7 @@ M.HoverPanel {
// Empty state // Empty state
Text { Text {
visible: M.NotifService.count === 0 visible: S.NotifService.count === 0
width: menuWindow.contentWidth width: menuWindow.contentWidth
height: 48 height: 48
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter

View file

@ -10,7 +10,7 @@ PanelWindow {
required property var screen required property var screen
visible: M.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen visible: S.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen
color: "transparent" color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
@ -34,7 +34,7 @@ PanelWindow {
property var _knownIds: ({}) property var _knownIds: ({})
Repeater { Repeater {
model: M.NotifService.popups.slice(0, S.Modules.notifications.maxPopups || 4) model: S.NotifService.popups.slice(0, S.Modules.notifications.maxPopups || 4)
delegate: Item { delegate: Item {
id: popupItem id: popupItem
@ -74,7 +74,7 @@ PanelWindow {
} }
Connections { Connections {
target: M.NotifService target: S.NotifService
function onPopupExpiring(notifId) { function onPopupExpiring(notifId) {
if (notifId === popupItem.modelData.id) if (notifId === popupItem.modelData.id)
popupItem.animateDismiss(false); popupItem.animateDismiss(false);
@ -152,7 +152,7 @@ PanelWindow {
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
ScriptAction { ScriptAction {
script: popupItem._fullDismiss ? M.NotifService.dismiss(popupItem.modelData.id) : M.NotifService.dismissPopup(popupItem.modelData.id) script: popupItem._fullDismiss ? S.NotifService.dismiss(popupItem.modelData.id) : S.NotifService.dismissPopup(popupItem.modelData.id)
} }
} }

View file

@ -8,28 +8,28 @@ M.BarSection {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: { tooltip: {
const parts = [M.NotifService.count + " notification" + (M.NotifService.count !== 1 ? "s" : "")]; const parts = [S.NotifService.count + " notification" + (S.NotifService.count !== 1 ? "s" : "")];
if (M.NotifService.dnd) if (S.NotifService.dnd)
parts.push("Do not disturb"); parts.push("Do not disturb");
return parts.join("\n"); return parts.join("\n");
} }
required property var bar required property var bar
readonly property bool hasUrgent: M.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed") readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
M.BarIcon { M.BarIcon {
icon: { icon: {
if (M.NotifService.dnd) if (S.NotifService.dnd)
return M.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93"; return S.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
return M.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C"; return S.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C";
} }
color: M.NotifService.dnd ? S.Theme.base04 : root.accentColor color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
M.BarLabel { M.BarLabel {
id: countLabel id: countLabel
label: M.NotifService.count > 0 ? String(M.NotifService.count) + (root.hasUrgent ? "!" : "") : "" label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
color: root.hasUrgent ? S.Theme.base08 : root.accentColor color: root.hasUrgent ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -61,9 +61,9 @@ M.BarSection {
} }
Connections { Connections {
target: M.NotifService target: S.NotifService
function onCountChanged() { function onCountChanged() {
if (M.NotifService.count > 0) if (S.NotifService.count > 0)
popAnim.start(); popAnim.start();
} }
} }
@ -77,7 +77,7 @@ M.BarSection {
} }
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onTapped: M.NotifService.toggleDnd() onTapped: S.NotifService.toggleDnd()
} }
LazyLoader { LazyLoader {

View file

@ -31,8 +31,6 @@ PowerProfileModule 1.0 PowerProfileModule.qml
IdleInhibitorModule 1.0 IdleInhibitorModule.qml IdleInhibitorModule 1.0 IdleInhibitorModule.qml
NotificationsModule 1.0 NotificationsModule.qml NotificationsModule 1.0 NotificationsModule.qml
ProcessList 1.0 ProcessList.qml ProcessList 1.0 ProcessList.qml
singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml
NotifPopup 1.0 NotifPopup.qml NotifPopup 1.0 NotifPopup.qml
NotifCenter 1.0 NotifCenter.qml NotifCenter 1.0 NotifCenter.qml
NotifCard 1.0 NotifCard.qml NotifCard 1.0 NotifCard.qml

View file

@ -95,6 +95,7 @@ QtObject {
property var lock: ({ property var lock: ({
enable: true, enable: true,
screenshot: true, screenshot: true,
notifications: true,
mpris: true, mpris: true,
volume: true volume: true
}) })

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "." as M import "." as S
import "../services" as S
QtObject { QtObject {
id: root id: root
@ -32,7 +31,7 @@ QtObject {
// Relative time string recomputed whenever NotifService._now ticks (single global 5s timer) // Relative time string recomputed whenever NotifService._now ticks (single global 5s timer)
readonly property string timeStr: { readonly property string timeStr: {
const diff = M.NotifService._now - time; const diff = S.NotifService._now - time;
const m = Math.floor(diff / 60000); const m = Math.floor(diff / 60000);
if (m < 1) if (m < 1)
return "now"; return "now";
@ -48,7 +47,7 @@ QtObject {
target: root.notification target: root.notification
function onClosed() { function onClosed() {
if (root.state !== "dismissed") if (root.state !== "dismissed")
M.NotifService.dismiss(root.id); S.NotifService.dismiss(root.id);
} }
} }

View file

@ -3,8 +3,7 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "." as M import "." as S
import "../services" as S
QtObject { QtObject {
id: root id: root

View file

@ -4,3 +4,5 @@ singleton SystemStats 1.0 SystemStats.qml
singleton Modules 1.0 Modules.qml singleton Modules 1.0 Modules.qml
singleton NiriIpc 1.0 NiriIpc.qml singleton NiriIpc 1.0 NiriIpc.qml
singleton PowerProfileService 1.0 PowerProfileService.qml singleton PowerProfileService 1.0 PowerProfileService.qml
singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml