From 03ff5aa3c69d17f2b83d2eab9d4e94b217486c27 Mon Sep 17 00:00:00 2001 From: Damocles Date: Thu, 16 Apr 2026 20:51:45 +0200 Subject: [PATCH 1/4] notifcenter: show urgent (critical) notifications at the top --- modules/NotifService.qml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/NotifService.qml b/modules/NotifService.qml index 0591eae..77b829e 100644 --- a/modules/NotifService.qml +++ b/modules/NotifService.qml @@ -90,7 +90,13 @@ QtObject { }); root._byId[item.id] = item; - root.list = [item, ...root.list]; + root.list = [item, ...root.list].sort((a, b) => { + const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0; + const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0; + if (aU !== bU) + return bU - aU; + return b.time - a.time; + }); // Trim excess popups const max = M.Modules.notifications.maxPopups || 4; @@ -174,6 +180,13 @@ QtObject { root._byId[item.id] = item; root.list.push(item); } + root.list.sort((a, b) => { + const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0; + const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0; + if (aU !== bU) + return bU - aU; + return b.time - a.time; + }); root._changed(); } catch (e) {} } From 1ae953fc73a0cb8628890bd1c22cbc98758b0b75 Mon Sep 17 00:00:00 2001 From: Damocles Date: Thu, 16 Apr 2026 20:53:15 +0200 Subject: [PATCH 2/4] hm-module: enable poweralertd by default with battery module (opt-out via battery.poweralertd = false) --- nix/hm-module.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nix/hm-module.nix b/nix/hm-module.nix index c2cb1a1..33cbc0f 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -152,6 +152,11 @@ in default = 15; description = "Battery percentage for critical notification and blink."; }; + poweralertd = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable poweralertd for battery/power event notifications. Set to false to disable."; + }; }; weather = moduleOpt "weather" ( (intervalOpt 3600000) @@ -234,6 +239,10 @@ in } ]; + services.poweralertd.enable = lib.mkIf ( + cfg.modules.battery.enable && cfg.modules.battery.poweralertd + ) (lib.mkDefault true); + systemd.user.services.nova-shell = lib.mkIf cfg.systemd.enable { Unit = { Description = "nova-shell Quickshell bar"; From a878067e163c0f3e5015344ece456c4f4e14349b Mon Sep 17 00:00:00 2001 From: Damocles Date: Thu, 16 Apr 2026 20:55:10 +0200 Subject: [PATCH 3/4] mpris: crossfade album art on session switch --- modules/Mpris.qml | 65 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/modules/Mpris.qml b/modules/Mpris.qml index 0ffbbb7..f783643 100644 --- a/modules/Mpris.qml +++ b/modules/Mpris.qml @@ -122,7 +122,7 @@ M.BarSection { panelTitle: "Now Playing" contentWidth: 280 - // Album art — always 1:1 + // Album art — always 1:1, crossfades on session switch Item { width: parent.width height: width @@ -133,22 +133,71 @@ M.BarSection { color: M.Theme.base02 } + // Outgoing art — snaps to current opacity, then fades out + Image { + id: _artImgPrev + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0 + + NumberAnimation { + id: _prevFadeOut + target: _artImgPrev + property: "opacity" + to: 0 + duration: 300 + easing.type: Easing.InOutCubic + } + } + + // Incoming art — fades in once loaded Image { id: _artImg anchors.fill: parent fillMode: Image.PreserveAspectCrop - visible: _hasArt asynchronous: true - source: root._cachedArt + opacity: 0 property bool _hasArt: false - onStatusChanged: if (status === Image.Ready) - _hasArt = true + + onStatusChanged: { + if (status === Image.Ready && source !== "") { + _hasArt = true; + _artFadeIn.start(); + _prevFadeOut.start(); + } else if (status === Image.Error) { + _hasArt = false; + } + } + + NumberAnimation { + id: _artFadeIn + target: _artImg + property: "opacity" + to: 1 + duration: 300 + easing.type: Easing.InOutCubic + } + Connections { target: root function on_CachedArtChanged() { - if (!root._cachedArt) + if (!root._cachedArt) { + _artFadeIn.stop(); + _prevFadeOut.stop(); _artImg._hasArt = false; + _artImg.opacity = 0; + _artImgPrev.opacity = 0; + _artImg.source = ""; + } else if (root._cachedArt !== _artImg.source) { + _prevFadeOut.stop(); + _artFadeIn.stop(); + _artImgPrev.source = _artImg.source; + _artImgPrev.opacity = _artImg.opacity; + _artImg.opacity = 0; + _artImg.source = root._cachedArt; + } } } } @@ -187,7 +236,7 @@ M.BarSection { anchors.right: parent.right anchors.bottom: parent.bottom height: 40 - visible: _artImg.visible + visible: _artImg._hasArt gradient: Gradient { GradientStop { position: 0 @@ -206,7 +255,7 @@ M.BarSection { color: M.Theme.base04 font.pixelSize: 28 font.family: M.Theme.iconFontFamily - visible: _artImg.status !== Image.Ready + visible: !_artImg._hasArt } } From 434f8f8ffd3801cfeac82f6caaf1b58ef25e9c11 Mon Sep 17 00:00:00 2001 From: Damocles Date: Thu, 16 Apr 2026 21:10:10 +0200 Subject: [PATCH 4/4] hoverpanel: fix popup dismiss race, replace click-outside mousearea with taphandler + grace guard; pinned panels draggable by header --- modules/HoverPanel.qml | 53 +++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/modules/HoverPanel.qml b/modules/HoverPanel.qml index 59192fa..59f1e44 100644 --- a/modules/HoverPanel.qml +++ b/modules/HoverPanel.qml @@ -43,14 +43,16 @@ PanelWindow { property bool _winVisible: false property bool _pinned: false + property real _dragStartX: 0 - // When pinned, only the pin button area receives input — everything else passes through + // When pinned, only the header bar receives input — content passes through to desktop. + // Full header width allows drag-to-reposition; pin button tap is within that region too. mask: _pinned ? _pinMask : null property Region _pinMask: Region { - x: panelContainer.x + panelContainer.width - 28 + x: panelContainer.x y: 0 - width: 28 + width: panelContainer.width height: 24 } @@ -190,12 +192,21 @@ PanelWindow { } } - // Popup mode: click-outside dismiss (declared first = lowest z = below content) - MouseArea { + // Popup mode: click-outside dismiss. + // TapHandler fires for all taps; position check skips taps inside panelContainer. + // Gated on !_grace so spurious events during the 400ms opening window don't dismiss. + Item { anchors.fill: parent visible: root.popupMode - enabled: root.popupMode - onClicked: root.dismiss() + + TapHandler { + enabled: !root._grace + onTapped: { + const p = point.position; + if (p.x < panelContainer.x || p.x > panelContainer.x + panelContainer.width || p.y < panelContainer.y || p.y > panelContainer.y + panelContainer.height) + root.dismiss(); + } + } } M.PopupBackground { @@ -215,13 +226,6 @@ PanelWindow { height: _panelColumn.height opacity: 0 - // Popup mode: eat clicks on panel background so outer dismiss doesn't fire - MouseArea { - anchors.fill: parent - visible: root.popupMode - enabled: root.popupMode - } - HoverHandler { enabled: !root.popupMode && !root._pinned onHoveredChanged: if (!root.popupMode && !root._pinned) @@ -235,10 +239,31 @@ PanelWindow { // Header row: title + action buttons + pin — shown in hover mode always, // and in popup mode when a title or actions are provided. Item { + id: _headerItem visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null width: parent.width height: 24 + // Drag header to reposition panel while pinned (hover mode only) + DragHandler { + enabled: root._pinned && !root.popupMode + yAxis.enabled: false + onActiveChanged: if (active) + root._dragStartX = panelContainer.x + onActiveTranslationChanged: { + if (active) { + const sw = root.screen?.width ?? 1920; + panelContainer.x = Math.max(0, Math.min(root._dragStartX + activeTranslation.x, sw - root.contentWidth)); + } + } + } + + // Show move cursor on header when pinned + HoverHandler { + enabled: root._pinned && !root.popupMode + cursorShape: Qt.SizeHorCursor + } + Text { visible: root.panelTitle !== "" anchors.left: parent.left