diff --git a/modules/Modules.qml b/modules/Modules.qml index 3b5f0e9..3c0e700 100644 --- a/modules/Modules.qml +++ b/modules/Modules.qml @@ -22,7 +22,8 @@ QtObject { property var notifications: ({ enable: true, timeout: 3000, - maxPopups: 4 + maxPopups: 4, + maxVisible: 10 }) property var mpris: ({ enable: true diff --git a/modules/NotifCenter.qml b/modules/NotifCenter.qml index c96de2d..3014fc0 100644 --- a/modules/NotifCenter.qml +++ b/modules/NotifCenter.qml @@ -131,240 +131,261 @@ M.PopupPanel { color: M.Theme.base03 } - // Notification list - Repeater { - model: M.NotifService.list.slice(0, 20) + // Notification list (scrollable) + Item { + width: menuWindow.panelWidth + height: Math.min(notifFlick.contentHeight, _maxHeight) + readonly property real _itemHeight: 60 + readonly property real _maxHeight: _itemHeight * (M.Modules.notifications.maxVisible || 10) - delegate: Item { - id: notifItem - required property var modelData - required property int index - - width: menuWindow.panelWidth - height: _targetHeight * _heightScale - opacity: 0 + Flickable { + id: notifFlick + anchors.fill: parent + contentWidth: width + contentHeight: notifCol.implicitHeight clip: true - - readonly property real _targetHeight: notifContent.height + 12 - property real _heightScale: 1 - property bool _skipDismiss: false - - function dismiss() { - _dismissAnim.start(); - } - - function dismissVisualOnly() { - _skipDismiss = true; - _dismissAnim.start(); - } - - Component.onCompleted: { - menuWindow._delegates.push(notifItem); - fadeIn.start(); - } - Component.onDestruction: { - const idx = menuWindow._delegates.indexOf(notifItem); - if (idx >= 0) - menuWindow._delegates.splice(idx, 1); - } - - NumberAnimation { - id: fadeIn - target: notifItem - property: "opacity" - to: 1 - duration: 150 - easing.type: Easing.OutCubic - } - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: notifArea.containsMouse ? M.Theme.base02 : "transparent" - radius: M.Theme.radius - } - - // Urgency accent - Rectangle { - anchors.left: parent.left - anchors.leftMargin: 4 - anchors.top: parent.top - anchors.topMargin: 4 - anchors.bottom: parent.bottom - anchors.bottomMargin: 4 - width: 2 - radius: 1 - color: { - const u = notifItem.modelData.urgency; - return u === NotificationUrgency.Critical ? M.Theme.base08 : u === NotificationUrgency.Low ? M.Theme.base04 : M.Theme.base0D; - } - } + boundsBehavior: Flickable.StopAtBounds Column { - id: notifContent - anchors.left: parent.left - anchors.right: dismissBtn.left - anchors.top: parent.top - anchors.leftMargin: 14 - anchors.topMargin: 6 - spacing: 1 + id: notifCol + width: parent.width - // App + time - Row { - width: parent.width - Text { - text: notifItem.modelData.appName || "Notification" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - ageText.width - 4 - } - Text { - id: ageText - text: { - const diff = Math.floor((Date.now() - notifItem.modelData.time) / 60000); - if (diff < 1) - return "now"; - if (diff < 60) - return diff + "m"; - if (diff < 1440) - return Math.floor(diff / 60) + "h"; - return Math.floor(diff / 1440) + "d"; + Repeater { + model: M.NotifService.list + + delegate: Item { + id: notifItem + required property var modelData + required property int index + + width: menuWindow.panelWidth + height: _targetHeight * _heightScale + opacity: 0 + clip: true + + readonly property real _targetHeight: notifContent.height + 12 + property real _heightScale: 1 + property bool _skipDismiss: false + + function dismiss() { + _dismissAnim.start(); } - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - Text { - width: parent.width - text: notifItem.modelData.summary || "" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: true - elide: Text.ElideRight - } + function dismissVisualOnly() { + _skipDismiss = true; + _dismissAnim.start(); + } - Text { - width: parent.width - text: notifItem.modelData.body || "" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text !== "" - } + Component.onCompleted: { + menuWindow._delegates.push(notifItem); + fadeIn.start(); + } + Component.onDestruction: { + const idx = menuWindow._delegates.indexOf(notifItem); + if (idx >= 0) + menuWindow._delegates.splice(idx, 1); + } - // Actions - Row { - spacing: 4 - visible: notifItem.modelData.actions && notifItem.modelData.actions.length > 0 + NumberAnimation { + id: fadeIn + target: notifItem + property: "opacity" + to: 1 + duration: 150 + easing.type: Easing.OutCubic + } - Repeater { - model: notifItem.modelData.actions || [] - delegate: Rectangle { - required property var modelData - width: actText.implicitWidth + 10 - height: actText.implicitHeight + 4 + Rectangle { + anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: notifArea.containsMouse ? M.Theme.base02 : "transparent" radius: M.Theme.radius - color: actArea.containsMouse ? M.Theme.base02 : "transparent" - border.color: M.Theme.base03 - border.width: 1 + } + + // Urgency accent + Rectangle { + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.top: parent.top + anchors.topMargin: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: 4 + width: 2 + radius: 1 + color: { + const u = notifItem.modelData.urgency; + return u === NotificationUrgency.Critical ? M.Theme.base08 : u === NotificationUrgency.Low ? M.Theme.base04 : M.Theme.base0D; + } + } + + Column { + id: notifContent + anchors.left: parent.left + anchors.right: dismissBtn.left + anchors.top: parent.top + anchors.leftMargin: 14 + anchors.topMargin: 6 + spacing: 1 + + // App + time + Row { + width: parent.width + Text { + text: notifItem.modelData.appName || "Notification" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - ageText.width - 4 + } + Text { + id: ageText + text: { + const diff = Math.floor((Date.now() - notifItem.modelData.time) / 60000); + if (diff < 1) + return "now"; + if (diff < 60) + return diff + "m"; + if (diff < 1440) + return Math.floor(diff / 60) + "h"; + return Math.floor(diff / 1440) + "d"; + } + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } Text { - id: actText - anchors.centerIn: parent - text: parent.modelData.text - color: M.Theme.base0D - font.pixelSize: M.Theme.fontSize - 2 + width: parent.width + text: notifItem.modelData.summary || "" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight } - MouseArea { - id: actArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - parent.modelData.invoke(); - M.NotifService.dismiss(notifItem.modelData.id); + + Text { + width: parent.width + text: notifItem.modelData.body || "" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + visible: text !== "" + } + + // Actions + Row { + spacing: 4 + visible: notifItem.modelData.actions && notifItem.modelData.actions.length > 0 + + Repeater { + model: notifItem.modelData.actions || [] + delegate: Rectangle { + required property var modelData + width: actText.implicitWidth + 10 + height: actText.implicitHeight + 4 + radius: M.Theme.radius + color: actArea.containsMouse ? M.Theme.base02 : "transparent" + border.color: M.Theme.base03 + border.width: 1 + + Text { + id: actText + anchors.centerIn: parent + text: parent.modelData.text + color: M.Theme.base0D + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + MouseArea { + id: actArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + parent.modelData.invoke(); + M.NotifService.dismiss(notifItem.modelData.id); + } + } + } } } } - } - } - } - // Dismiss button - Text { - id: dismissBtn - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.top: parent.top - anchors.topMargin: 8 - text: "\uF00D" - color: dismissArea.containsMouse ? M.Theme.base08 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.iconFontFamily + // Dismiss button + Text { + id: dismissBtn + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.top: parent.top + anchors.topMargin: 8 + text: "\uF00D" + color: dismissArea.containsMouse ? M.Theme.base08 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.iconFontFamily - MouseArea { - id: dismissArea - anchors.fill: parent - anchors.margins: -4 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: _dismissAnim.start() - } - } + MouseArea { + id: dismissArea + anchors.fill: parent + anchors.margins: -4 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: _dismissAnim.start() + } + } - SequentialAnimation { - id: _dismissAnim - ParallelAnimation { - NumberAnimation { - target: notifItem - property: "x" - to: menuWindow.panelWidth - duration: 200 - easing.type: Easing.InCubic - } - NumberAnimation { - target: notifItem - property: "opacity" - to: 0 - duration: 200 - easing.type: Easing.InCubic - } - } - NumberAnimation { - target: notifItem - property: "_heightScale" - to: 0 - duration: 150 - easing.type: Easing.OutCubic - } - ScriptAction { - script: { - if (!notifItem._skipDismiss) - M.NotifService.dismiss(notifItem.modelData.id); - } - } - } + SequentialAnimation { + id: _dismissAnim + ParallelAnimation { + NumberAnimation { + target: notifItem + property: "x" + to: menuWindow.panelWidth + duration: 200 + easing.type: Easing.InCubic + } + NumberAnimation { + target: notifItem + property: "opacity" + to: 0 + duration: 200 + easing.type: Easing.InCubic + } + } + NumberAnimation { + target: notifItem + property: "_heightScale" + to: 0 + duration: 150 + easing.type: Easing.OutCubic + } + ScriptAction { + script: { + if (!notifItem._skipDismiss) + M.NotifService.dismiss(notifItem.modelData.id); + } + } + } - MouseArea { - id: notifArea - anchors.fill: parent - z: -1 - hoverEnabled: true - acceptedButtons: Qt.RightButton - onClicked: _dismissAnim.start() - } - } - } + MouseArea { + id: notifArea + anchors.fill: parent + z: -1 + hoverEnabled: true + acceptedButtons: Qt.RightButton + onClicked: _dismissAnim.start() + } + } + } // Repeater + } // Column + } // Flickable + } // Item // Empty state Text { diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 35a68e4..9c0f093 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -103,6 +103,11 @@ in default = 4; description = "Maximum number of notification popups shown simultaneously."; }; + maxVisible = lib.mkOption { + type = lib.types.int; + default = 10; + description = "Maximum visible notifications in the notification center before scrolling."; + }; }; bluetooth = moduleOpt "bluetooth" (intervalOpt 5000); network = moduleOpt "network" (intervalOpt 5000);