diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center')
10 files changed, 3616 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml new file mode 100644 index 0000000..5e033e5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml @@ -0,0 +1,277 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + required property var historyItem + property bool isSelected: false + property bool keyboardNavigationActive: false + property bool descriptionExpanded: NotificationService.expandedMessages[historyItem?.id ? (historyItem.id + "_hist") : ""] || false + property bool __initialized: false + + Component.onCompleted: { + Qt.callLater(() => { + if (root) + root.__initialized = true; + }); + } + + readonly property bool compactMode: SettingsData.notificationCompactMode + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real iconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal + readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS + readonly property real collapsedContentHeight: iconSize + cardPadding + readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + + width: parent ? parent.width : 400 + height: baseCardHeight + contentItem.extraHeight + radius: Theme.cornerRadius + clip: false + readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel1 + fallbackOffset: 1 + targetRadius: root.radius + targetColor: root.color + borderColor: root.border.color + borderWidth: root.border.width + shadowEnabled: root.shadowsAllowed + } + + color: { + if (isSelected && keyboardNavigationActive) + return Theme.primaryPressed; + return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); + } + border.color: { + if (isSelected && keyboardNavigationActive) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5); + if (historyItem.urgency === 2) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3); + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05); + } + border.width: { + if (isSelected && keyboardNavigationActive) + return 1.5; + if (historyItem.urgency === 2) + return 2; + return 0; + } + + Behavior on border.color { + enabled: root.__initialized + ColorAnimation { + duration: root.__initialized ? Theme.shortDuration : 0 + easing.type: Theme.standardEasing + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + visible: historyItem.urgency === 2 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0.0 + color: Theme.primary + } + GradientStop { + position: 0.02 + color: Theme.primary + } + GradientStop { + position: 0.021 + color: "transparent" + } + } + } + + Item { + id: contentItem + + readonly property real expandedTextHeight: descriptionText.contentHeight + readonly property real twoLineHeight: descriptionText.font.pixelSize * 1.2 * 2 + readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 0 + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: cardPadding + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin + height: collapsedContentHeight + extraHeight + + DankCircularImage { + id: iconContainer + readonly property string rawImage: historyItem.image || "" + readonly property string iconFromImage: { + if (rawImage.startsWith("image://icon/")) + return rawImage.substring(13); + return ""; + } + readonly property bool imageHasSpecialPrefix: { + const icon = iconFromImage; + return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:"); + } + readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/") + + width: iconSize + height: iconSize + anchors.left: parent.left + anchors.top: parent.top + + imageSource: { + if (hasNotificationImage) + return historyItem.image; + if (imageHasSpecialPrefix) + return ""; + const appIcon = historyItem.appIcon; + if (!appIcon) + return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) + return appIcon; + if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) + return ""; + return Paths.resolveIconPath(appIcon); + } + + hasImage: hasNotificationImage + fallbackIcon: { + if (imageHasSpecialPrefix) + return iconFromImage; + return historyItem.appIcon || iconFromImage || ""; + } + fallbackText: { + const appName = historyItem.appName || "?"; + return appName.charAt(0).toUpperCase(); + } + + Rectangle { + anchors.fill: parent + anchors.margins: -2 + radius: width / 2 + color: "transparent" + border.color: root.color + border.width: 5 + visible: parent.hasImage + antialiasing: true + } + } + + Rectangle { + anchors.left: iconContainer.right + anchors.leftMargin: Theme.spacingM + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: contentSpacing + color: "transparent" + + Column { + width: parent.width + anchors.top: parent.top + spacing: Theme.notificationContentSpacing + + Row { + width: parent.width + spacing: Theme.spacingXS + readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing + + StyledText { + id: historyTitleText + width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) + text: { + let title = historyItem.summary || ""; + const appName = historyItem.appName || ""; + const prefix = appName + " • "; + if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { + title = title.substring(prefix.length); + } + return title; + } + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { + id: historySeparator + text: (historyTitleText.text.length > 0 && historyTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + StyledText { + id: historyTimeText + text: NotificationService.formatHistoryTime(historyItem.timestamp) + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + visible: text.length > 0 + } + } + + StyledText { + id: descriptionText + property bool hasMoreText: truncated + + text: historyItem.htmlBody || historyItem.body || "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) { + const messageId = historyItem?.id ? (historyItem.id + "_hist") : ""; + NotificationService.toggleMessageExpansion(messageId); + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) + mouse.accepted = false; + } + onReleased: mouse => { + if (parent.hoveredLink) + mouse.accepted = false; + } + } + } + } + } + } + + DankActionButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: cardPadding + anchors.rightMargin: Theme.spacingL + iconName: "close" + iconSize: compactMode ? 16 : 18 + buttonSize: compactMode ? 24 : 28 + onClicked: NotificationService.removeFromHistory(historyItem.id) + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml new file mode 100644 index 0000000..69fea41 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml @@ -0,0 +1,387 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string selectedFilterKey: "all" + property var keyboardController: null + property bool keyboardActive: false + property int selectedIndex: -1 + property bool showKeyboardHints: false + + function getStartOfDay(date) { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; + } + + function getFilterRange(key) { + const now = new Date(); + const startOfToday = getStartOfDay(now); + const startOfYesterday = new Date(startOfToday.getTime() - 86400000); + + switch (key) { + case "all": + return { + start: null, + end: null + }; + case "1h": + return { + start: new Date(now.getTime() - 3600000), + end: null + }; + case "today": + return { + start: startOfToday, + end: null + }; + case "yesterday": + return { + start: startOfYesterday, + end: startOfToday + }; + case "older": + return { + start: null, + end: getOlderCutoff() + }; + case "7d": + return { + start: new Date(now.getTime() - 7 * 86400000), + end: null + }; + case "30d": + return { + start: new Date(now.getTime() - 30 * 86400000), + end: null + }; + default: + return { + start: null, + end: null + }; + } + } + + function countForFilter(key) { + const range = getFilterRange(key); + if (!range.start && !range.end) + return NotificationService.historyList.length; + return NotificationService.historyList.filter(n => { + const ts = n.timestamp; + if (range.start && ts < range.start.getTime()) + return false; + if (range.end && ts >= range.end.getTime()) + return false; + return true; + }).length; + } + + readonly property var allFilters: [ + { + label: I18n.tr("All", "notification history filter"), + key: "all", + maxDays: 0 + }, + { + label: I18n.tr("Last hour", "notification history filter"), + key: "1h", + maxDays: 1 + }, + { + label: I18n.tr("Today", "notification history filter"), + key: "today", + maxDays: 1 + }, + { + label: I18n.tr("Yesterday", "notification history filter"), + key: "yesterday", + maxDays: 2 + }, + { + label: I18n.tr("7 days", "notification history filter"), + key: "7d", + maxDays: 7 + }, + { + label: I18n.tr("30 days", "notification history filter"), + key: "30d", + maxDays: 30 + }, + { + label: I18n.tr("Older", "notification history filter for content older than other filters"), + key: "older", + maxDays: 0 + } + ] + + function filterRelevantForRetention(filter) { + const retention = SettingsData.notificationHistoryMaxAgeDays; + if (filter.key === "older") { + if (retention === 0) + return true; + return retention > 2 && retention < 7 || retention > 30; + } + if (retention === 0) + return true; + if (filter.maxDays === 0) + return true; + return filter.maxDays <= retention; + } + + function getOlderCutoff() { + const retention = SettingsData.notificationHistoryMaxAgeDays; + const now = new Date(); + if (retention === 0 || retention > 30) + return new Date(now.getTime() - 30 * 86400000); + if (retention >= 7) + return new Date(now.getTime() - 7 * 86400000); + const startOfToday = getStartOfDay(now); + return new Date(startOfToday.getTime() - 86400000); + } + + readonly property var visibleFilters: { + const result = []; + const retention = SettingsData.notificationHistoryMaxAgeDays; + for (let i = 0; i < allFilters.length; i++) { + const f = allFilters[i]; + if (!filterRelevantForRetention(f)) + continue; + const count = countForFilter(f.key); + if (f.key === "all" || count > 0) { + result.push({ + label: f.label, + key: f.key, + count: count + }); + } + } + return result; + } + + onVisibleFiltersChanged: { + let found = false; + for (let i = 0; i < visibleFilters.length; i++) { + if (visibleFilters[i].key === selectedFilterKey) { + found = true; + break; + } + } + if (!found) + selectedFilterKey = "all"; + } + + function getFilteredHistory() { + const range = getFilterRange(selectedFilterKey); + if (!range.start && !range.end) + return NotificationService.historyList; + return NotificationService.historyList.filter(n => { + const ts = n.timestamp; + if (range.start && ts < range.start.getTime()) + return false; + if (range.end && ts >= range.end.getTime()) + return false; + return true; + }); + } + + function getChipIndex() { + for (let i = 0; i < visibleFilters.length; i++) { + if (visibleFilters[i].key === selectedFilterKey) + return i; + } + return 0; + } + + function enableAutoScroll() { + } + + function removeWithScrollPreserve(itemId) { + historyListView.savedY = historyListView.contentY; + NotificationService.removeFromHistory(itemId); + Qt.callLater(() => { + historyListView.forceLayout(); + }); + } + + Column { + anchors.fill: parent + spacing: Theme.spacingS + + DankFilterChips { + id: filterChips + width: parent.width + currentIndex: root.getChipIndex() + showCounts: true + model: root.visibleFilters + onSelectionChanged: index => { + if (index >= 0 && index < root.visibleFilters.length) { + root.selectedFilterKey = root.visibleFilters[index].key; + } + } + } + + DankListView { + id: historyListView + width: parent.width + height: parent.height - filterChips.height - Theme.spacingS + clip: true + spacing: Theme.spacingS + readonly property real horizontalShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) + readonly property real verticalShadowGutter: Theme.snap(Math.max(Theme.spacingS, 8), 1) + readonly property real delegateShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) + topMargin: verticalShadowGutter + bottomMargin: verticalShadowGutter + + model: ScriptModel { + id: historyModel + values: root.getFilteredHistory() + objectProp: "id" + } + + NotificationEmptyState { + visible: historyListView.count === 0 + y: Theme.spacingL + anchors.horizontalCenter: parent.horizontalCenter + } + + delegate: Item { + id: delegateRoot + required property var modelData + required property int index + + property real swipeOffset: 0 + property bool isDismissing: false + readonly property real dismissThreshold: width * 0.35 + property bool __delegateInitialized: false + + Component.onCompleted: { + Qt.callLater(() => { + if (delegateRoot) + delegateRoot.__delegateInitialized = true; + }); + } + + width: ListView.view.width + height: historyCard.height + historyListView.delegateShadowGutter + clip: false + + HistoryNotificationCard { + id: historyCard + width: Math.max(0, parent.width - (historyListView.horizontalShadowGutter * 2)) + y: historyListView.delegateShadowGutter / 2 + x: historyListView.horizontalShadowGutter + delegateRoot.swipeOffset + historyItem: modelData + isSelected: root.keyboardActive && root.selectedIndex === index + keyboardNavigationActive: root.keyboardActive + opacity: 1 - Math.abs(delegateRoot.swipeOffset) / (delegateRoot.width * 0.5) + + Behavior on x { + enabled: !swipeDragHandler.active && delegateRoot.__delegateInitialized + NumberAnimation { + duration: Theme.notificationExitDuration + easing.type: Theme.standardEasing + } + } + + Behavior on opacity { + enabled: delegateRoot.__delegateInitialized + NumberAnimation { + duration: delegateRoot.__delegateInitialized ? Theme.notificationExitDuration : 0 + } + } + } + + DragHandler { + id: swipeDragHandler + target: null + yAxis.enabled: false + xAxis.enabled: true + + onActiveChanged: { + if (active || delegateRoot.isDismissing) + return; + if (Math.abs(delegateRoot.swipeOffset) > delegateRoot.dismissThreshold) { + delegateRoot.isDismissing = true; + root.removeWithScrollPreserve(delegateRoot.modelData?.id || ""); + } else { + delegateRoot.swipeOffset = 0; + } + } + + onTranslationChanged: { + if (delegateRoot.isDismissing) + return; + delegateRoot.swipeOffset = translation.x; + } + } + } + } + } + + function selectNext() { + if (historyModel.values.length === 0) + return; + keyboardActive = true; + selectedIndex = Math.min(selectedIndex + 1, historyModel.values.length - 1); + historyListView.positionViewAtIndex(selectedIndex, ListView.Contain); + } + + function selectPrevious() { + if (historyModel.values.length === 0) + return; + if (selectedIndex <= 0) { + keyboardActive = false; + selectedIndex = -1; + return; + } + selectedIndex = Math.max(selectedIndex - 1, 0); + historyListView.positionViewAtIndex(selectedIndex, ListView.Contain); + } + + function clearSelected() { + if (selectedIndex < 0 || selectedIndex >= historyModel.values.length) + return; + const item = historyModel.values[selectedIndex]; + NotificationService.removeFromHistory(item.id); + if (historyModel.values.length === 0) { + keyboardActive = false; + selectedIndex = -1; + } else { + selectedIndex = Math.min(selectedIndex, historyModel.values.length - 1); + } + } + + function handleKey(event) { + if (event.key === Qt.Key_Down || event.key === 16777237) { + if (!keyboardActive) { + keyboardActive = true; + selectedIndex = 0; + } else { + selectNext(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Up || event.key === 16777235) { + if (keyboardActive) { + selectPrevious(); + } + event.accepted = true; + } else if (keyboardActive && (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace)) { + clearSelected(); + event.accepted = true; + } else if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) { + NotificationService.clearHistory(); + keyboardActive = false; + selectedIndex = -1; + event.accepted = true; + } else if (event.key === Qt.Key_F10) { + showKeyboardHints = !showKeyboardHints; + event.accepted = true; + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml new file mode 100644 index 0000000..855163d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -0,0 +1,311 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankListView { + id: listView + + property var keyboardController: null + property bool keyboardActive: false + property bool autoScrollDisabled: false + property bool isAnimatingExpansion: false + property alias listContentHeight: listView.contentHeight + property real stableContentHeight: 0 + property bool cardAnimateExpansion: true + property bool listInitialized: false + property int swipingCardIndex: -1 + property real swipingCardOffset: 0 + property real __pendingStableHeight: 0 + property real __heightUpdateThreshold: 20 + readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0 + readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1) + readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1) + readonly property real delegateShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) + + Component.onCompleted: { + Qt.callLater(() => { + if (listView) { + listView.listInitialized = true; + listView.stableContentHeight = listView.contentHeight; + } + }); + } + + Timer { + id: heightUpdateDebounce + interval: Theme.mediumDuration + 20 + repeat: false + onTriggered: { + if (!listView.isAnimatingExpansion && Math.abs(listView.__pendingStableHeight - listView.stableContentHeight) > listView.__heightUpdateThreshold) { + listView.stableContentHeight = listView.__pendingStableHeight; + } + } + } + + onContentHeightChanged: { + if (!isAnimatingExpansion) { + __pendingStableHeight = contentHeight; + if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) { + heightUpdateDebounce.restart(); + } else { + stableContentHeight = contentHeight; + } + } + } + + onIsAnimatingExpansionChanged: { + if (isAnimatingExpansion) { + heightUpdateDebounce.stop(); + let delta = 0; + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item && item.children[0] && item.children[0].isAnimating) { + const targetDelegateHeight = item.children[0].targetHeight + listView.delegateShadowGutter; + delta += targetDelegateHeight - item.height; + } + } + const targetHeight = contentHeight + delta; + // During expansion, always update immediately without threshold check + stableContentHeight = targetHeight; + } else { + __pendingStableHeight = contentHeight; + heightUpdateDebounce.stop(); + stableContentHeight = __pendingStableHeight; + } + } + + clip: true + model: NotificationService.groupedNotifications + spacing: Theme.spacingL + topMargin: shadowVerticalGutter + bottomMargin: shadowVerticalGutter + + onIsUserScrollingChanged: { + if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) { + autoScrollDisabled = true; + } + } + + function enableAutoScroll() { + autoScrollDisabled = false; + } + + Timer { + id: positionPreservationTimer + interval: 200 + running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion + repeat: true + onTriggered: { + if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion) { + keyboardController.ensureVisible(); + } + } + } + + Timer { + id: expansionEnsureVisibleTimer + interval: Theme.mediumDuration + 50 + repeat: false + onTriggered: { + if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { + keyboardController.ensureVisible(); + } + } + } + + NotificationEmptyState { + visible: listView.count === 0 + y: 20 + anchors.horizontalCenter: parent.horizontalCenter + } + + onModelChanged: { + if (!keyboardController || !keyboardController.keyboardNavigationActive) { + return; + } + keyboardController.rebuildFlatNavigation(); + Qt.callLater(() => { + if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { + keyboardController.ensureVisible(); + } + }); + } + + delegate: Item { + id: delegateRoot + required property var modelData + required property int index + + readonly property bool isExpanded: (NotificationService.expandedGroups[modelData && modelData.key] || false) + property real swipeOffset: 0 + property bool isDismissing: false + readonly property real dismissThreshold: width * 0.35 + property bool __delegateInitialized: false + + readonly property bool isAdjacentToSwipe: listView.count >= 2 && listView.swipingCardIndex !== -1 && (index === listView.swipingCardIndex - 1 || index === listView.swipingCardIndex + 1) + readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? listView.swipingCardOffset * 0.10 : 0 + readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0 + readonly property real swipeFadeStartOffset: width * 0.75 + readonly property real swipeFadeDistance: Math.max(1, width - swipeFadeStartOffset) + + Component.onCompleted: { + Qt.callLater(() => { + if (delegateRoot) + delegateRoot.__delegateInitialized = true; + }); + } + + width: ListView.view.width + height: notificationCard.height + listView.delegateShadowGutter + clip: false + + NotificationCard { + id: notificationCard + width: Math.max(0, parent.width - (listView.shadowHorizontalGutter * 2)) + y: listView.delegateShadowGutter / 2 + x: listView.shadowHorizontalGutter + delegateRoot.swipeOffset + delegateRoot.adjacentSwipeInfluence + listLevelAdjacentScaleInfluence: delegateRoot.adjacentScaleInfluence + listLevelScaleAnimationsEnabled: listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe + notificationGroup: modelData + keyboardNavigationActive: listView.keyboardActive + animateExpansion: listView.cardAnimateExpansion && listView.listInitialized + opacity: { + const swipeAmount = Math.abs(delegateRoot.swipeOffset); + if (swipeAmount <= delegateRoot.swipeFadeStartOffset) + return 1; + const fadeProgress = (swipeAmount - delegateRoot.swipeFadeStartOffset) / delegateRoot.swipeFadeDistance; + return Math.max(0, 1 - fadeProgress); + } + onIsAnimatingChanged: { + if (isAnimating) { + listView.isAnimatingExpansion = true; + } else { + Qt.callLater(() => { + if (!notificationCard || !listView) + return; + let anyAnimating = false; + for (let i = 0; i < listView.count; i++) { + const item = listView.itemAtIndex(i); + if (item && item.children[0] && item.children[0].isAnimating) { + anyAnimating = true; + break; + } + } + listView.isAnimatingExpansion = anyAnimating; + }); + } + } + + isGroupSelected: { + if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) + return false; + keyboardController.selectionVersion; + const selection = keyboardController.getCurrentSelection(); + return selection.type === "group" && selection.groupIndex === index; + } + + selectedNotificationIndex: { + if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) + return -1; + keyboardController.selectionVersion; + const selection = keyboardController.getCurrentSelection(); + return (selection.type === "notification" && selection.groupIndex === index) ? selection.notificationIndex : -1; + } + + Behavior on x { + enabled: !swipeDragHandler.active && !delegateRoot.isDismissing && (listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe) && listView.listInitialized + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on opacity { + enabled: listView.listInitialized + NumberAnimation { + duration: listView.listInitialized ? Theme.shortDuration : 0 + } + } + } + + DragHandler { + id: swipeDragHandler + target: null + yAxis.enabled: false + xAxis.enabled: true + + onActiveChanged: { + if (active) { + listView.swipingCardIndex = index; + return; + } + listView.swipingCardIndex = -1; + listView.swipingCardOffset = 0; + if (delegateRoot.isDismissing) + return; + if (Math.abs(delegateRoot.swipeOffset) > delegateRoot.dismissThreshold) { + delegateRoot.isDismissing = true; + swipeDismissAnim.to = delegateRoot.swipeOffset > 0 ? delegateRoot.width : -delegateRoot.width; + swipeDismissAnim.start(); + } else { + delegateRoot.swipeOffset = 0; + } + } + + onTranslationChanged: { + if (delegateRoot.isDismissing) + return; + delegateRoot.swipeOffset = translation.x; + listView.swipingCardOffset = translation.x; + } + } + + NumberAnimation { + id: swipeDismissAnim + target: delegateRoot + property: "swipeOffset" + to: 0 + duration: Theme.notificationExitDuration + easing.type: Easing.OutCubic + onStopped: NotificationService.dismissGroup(delegateRoot.modelData?.key || "") + } + } + + Connections { + target: NotificationService + + function onGroupedNotificationsChanged() { + if (!keyboardController) { + return; + } + + if (keyboardController.isTogglingGroup) { + keyboardController.rebuildFlatNavigation(); + return; + } + + keyboardController.rebuildFlatNavigation(); + + if (keyboardController.keyboardNavigationActive) { + Qt.callLater(() => { + if (!autoScrollDisabled) { + keyboardController.ensureVisible(); + } + }); + } + } + + function onExpandedGroupsChanged() { + if (!keyboardController || !keyboardController.keyboardNavigationActive) + return; + expansionEnsureVisibleTimer.restart(); + } + + function onExpandedMessagesChanged() { + if (!keyboardController || !keyboardController.keyboardNavigationActive) + return; + expansionEnsureVisibleTimer.restart(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCard.qml new file mode 100644 index 0000000..7e404df --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -0,0 +1,1073 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Services.Notifications +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var notificationGroup + property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false) + property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false) + property bool userInitiatedExpansion: false + property bool isAnimating: false + property bool animateExpansion: true + + property bool isGroupSelected: false + property int selectedNotificationIndex: -1 + property bool keyboardNavigationActive: false + property int swipingNotificationIndex: -1 + property real swipingNotificationOffset: 0 + property real listLevelAdjacentScaleInfluence: 1.0 + property bool listLevelScaleAnimationsEnabled: true + + readonly property bool compactMode: SettingsData.notificationCompactMode + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real iconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal + readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS + readonly property real collapsedDismissOffset: 5 + readonly property real badgeSize: compactMode ? 16 : 18 + readonly property real actionButtonHeight: compactMode ? 20 : 24 + readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) + readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + + width: parent ? parent.width : 400 + height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) + readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) + radius: Theme.cornerRadius + scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence + readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + readonly property var shadowElevation: Theme.elevationLevel1 + readonly property real baseShadowBlurPx: (shadowElevation && shadowElevation.blurPx !== undefined) ? shadowElevation.blurPx : 4 + readonly property real hoverShadowBlurBoost: cardHoverHandler.hovered ? Math.min(2, baseShadowBlurPx * 0.25) : 0 + property real shadowBlurPx: shadowsAllowed ? (baseShadowBlurPx + hoverShadowBlurBoost) : 0 + property real shadowOffsetXPx: shadowsAllowed ? Theme.elevationOffsetX(shadowElevation) : 0 + property real shadowOffsetYPx: shadowsAllowed ? (Theme.elevationOffsetY(shadowElevation, 1) + (cardHoverHandler.hovered ? 0.35 : 0)) : 0 + property bool __initialized: false + + Component.onCompleted: { + Qt.callLater(() => { + if (root) + root.__initialized = true; + }); + } + + Behavior on scale { + enabled: listLevelScaleAnimationsEnabled + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowBlurPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowOffsetXPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowOffsetYPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + enabled: root.__initialized + ColorAnimation { + duration: root.__initialized ? Theme.shortDuration : 0 + easing.type: Theme.standardEasing + } + } + + color: { + if (isGroupSelected && keyboardNavigationActive) { + return Theme.primaryPressed; + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return Theme.primaryHoverLight; + } + return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); + } + border.color: { + if (isGroupSelected && keyboardNavigationActive) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5); + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); + } + if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3); + } + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05); + } + border.width: { + if (isGroupSelected && keyboardNavigationActive) { + return 1.5; + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return 1; + } + if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { + return 2; + } + return 0; + } + clip: false + + HoverHandler { + id: cardHoverHandler + } + + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: root.shadowElevation + targetRadius: root.radius + targetColor: root.color + borderColor: root.border.color + borderWidth: root.border.width + shadowBlurPx: root.shadowBlurPx + shadowSpreadPx: 0 + shadowOffsetX: root.shadowOffsetXPx + shadowOffsetY: root.shadowOffsetYPx + shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : "transparent" + shadowEnabled: root.shadowsAllowed + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + visible: notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0.0 + color: Theme.primary + } + GradientStop { + position: 0.02 + color: Theme.primary + } + GradientStop { + position: 0.021 + color: "transparent" + } + } + opacity: 1.0 + } + + Item { + id: collapsedContent + + readonly property real expandedTextHeight: descriptionText.contentHeight + readonly property real collapsedLineCount: compactMode ? 1 : 2 + readonly property real collapsedLineHeight: Theme.fontSizeSmall * 1.2 * collapsedLineCount + readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedLineHeight + 2) ? (expandedTextHeight - collapsedLineHeight) : 0 + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: cardPadding + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin + height: collapsedContentHeight + extraHeight + visible: !expanded + + DankCircularImage { + id: iconContainer + readonly property string rawImage: notificationGroup?.latestNotification?.image || "" + readonly property string iconFromImage: { + if (rawImage.startsWith("image://icon/")) + return rawImage.substring(13); + return ""; + } + readonly property bool imageHasSpecialPrefix: { + const icon = iconFromImage; + return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:"); + } + readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/") + + width: iconSize + height: iconSize + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: descriptionExpanded ? Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - iconSize / 2) : Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - iconSize / 2) + + imageSource: { + if (hasNotificationImage) + return notificationGroup.latestNotification.cleanImage; + if (imageHasSpecialPrefix) + return ""; + const appIcon = notificationGroup?.latestNotification?.appIcon; + if (!appIcon) + return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) + return appIcon; + if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) + return ""; + return Paths.resolveIconPath(appIcon); + } + + hasImage: hasNotificationImage + fallbackIcon: { + if (imageHasSpecialPrefix) + return iconFromImage; + return notificationGroup?.latestNotification?.appIcon || iconFromImage || ""; + } + fallbackText: { + const appName = notificationGroup?.appName || "?"; + return appName.charAt(0).toUpperCase(); + } + + Rectangle { + anchors.fill: parent + anchors.margins: -2 + radius: width / 2 + color: "transparent" + border.color: root.color + border.width: 5 + visible: parent.hasImage + antialiasing: true + } + + Rectangle { + width: badgeSize + height: badgeSize + radius: badgeSize / 2 + color: Theme.primary + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: -2 + anchors.rightMargin: -2 + visible: (notificationGroup?.count || 0) > 1 + + StyledText { + anchors.centerIn: parent + text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() + color: Theme.primaryText + font.pixelSize: compactMode ? 8 : 9 + font.weight: Font.Bold + } + } + } + + Rectangle { + id: textContainer + + anchors.left: iconContainer.right + anchors.leftMargin: Theme.spacingM + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: contentSpacing + color: "transparent" + + Column { + width: parent.width + anchors.top: parent.top + spacing: Theme.notificationContentSpacing + + Row { + id: collapsedHeaderRow + width: parent.width + spacing: Theme.spacingXS + visible: (collapsedHeaderAppNameText.text.length > 0 || collapsedHeaderTimeText.text.length > 0) + + StyledText { + id: collapsedHeaderAppNameText + text: notificationGroup?.appName || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, parent.width - collapsedHeaderSeparator.implicitWidth - collapsedHeaderTimeText.implicitWidth - parent.spacing * 2) + } + + StyledText { + id: collapsedHeaderSeparator + text: (collapsedHeaderAppNameText.text.length > 0 && collapsedHeaderTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + + StyledText { + id: collapsedHeaderTimeText + text: notificationGroup?.latestNotification?.timeStr || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + } + + StyledText { + id: collapsedTitleText + width: parent.width + text: notificationGroup?.latestNotification?.summary || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + id: descriptionText + property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || "" + property bool hasMoreText: truncated + + text: fullText + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: Text.ElideRight + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) { + root.userInitiatedExpansion = true; + const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""; + NotificationService.toggleMessageExpansion(messageId); + Qt.callLater(() => { + if (root && !root.isAnimating) + root.userInitiatedExpansion = false; + }); + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) + mouse.accepted = false; + } + onReleased: mouse => { + if (parent.hoveredLink) + mouse.accepted = false; + } + } + } + } + } + } + + Column { + id: expandedContent + objectName: "expandedContent" + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: cardPadding + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: compactMode ? Theme.spacingXS : Theme.spacingS + visible: expanded + + Item { + width: parent.width + height: compactMode ? 32 : 40 + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: notificationGroup?.appName || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + } + + Rectangle { + width: badgeSize + height: badgeSize + radius: badgeSize / 2 + color: Theme.primary + visible: (notificationGroup?.count || 0) > 1 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + anchors.centerIn: parent + text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() + color: Theme.primaryText + font.pixelSize: compactMode ? 8 : 9 + font.weight: Font.Bold + } + } + } + } + + Column { + width: parent.width + spacing: compactMode ? Theme.spacingS : Theme.spacingL + + Repeater { + id: notificationRepeater + objectName: "notificationRepeater" + model: notificationGroup?.notifications?.slice(0, 10) || [] + + delegate: Item { + id: expandedDelegateWrapper + required property var modelData + required property int index + readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false + readonly property bool isSelected: root.selectedNotificationIndex === index + readonly property bool actionsVisible: true + readonly property real expandedIconSize: compactMode ? Theme.notificationExpandedIconSizeCompact : Theme.notificationExpandedIconSizeNormal + + HoverHandler { + id: expandedDelegateHoverHandler + } + readonly property real expandedItemPadding: compactMode ? Theme.spacingS : Theme.spacingM + readonly property real expandedBaseHeight: expandedItemPadding * 2 + Math.max(expandedIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * 2) + actionButtonHeight + contentSpacing * 2 + property bool __delegateInitialized: false + property real swipeOffset: 0 + property bool isDismissing: false + readonly property real dismissThreshold: width * 0.35 + + Component.onCompleted: { + Qt.callLater(() => { + if (expandedDelegateWrapper) + expandedDelegateWrapper.__delegateInitialized = true; + }); + } + + width: parent.width + height: delegateRect.height + clip: true + + Rectangle { + id: delegateRect + width: parent.width + + readonly property bool isAdjacentToSwipe: root.swipingNotificationIndex !== -1 && (expandedDelegateWrapper.index === root.swipingNotificationIndex - 1 || expandedDelegateWrapper.index === root.swipingNotificationIndex + 1) + readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? root.swipingNotificationOffset * 0.10 : 0 + readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(root.swipingNotificationOffset) / width * 0.02 : 1.0 + + x: expandedDelegateWrapper.swipeOffset + adjacentSwipeInfluence + scale: adjacentScaleInfluence + transformOrigin: Item.Center + + Behavior on x { + enabled: !expandedSwipeHandler.active && !expandedDelegateWrapper.isDismissing + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on scale { + enabled: !expandedSwipeHandler.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + height: { + if (!messageExpanded) + return expandedBaseHeight; + const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2; + if (bodyText.implicitHeight > twoLineHeight + 2) + return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight; + return expandedBaseHeight; + } + radius: Theme.cornerRadius + color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: 1 + + Behavior on border.color { + enabled: __delegateInitialized + ColorAnimation { + duration: __delegateInitialized ? Theme.shortDuration : 0 + easing.type: Theme.standardEasing + } + } + + Behavior on height { + enabled: false + } + + Item { + anchors.fill: parent + anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM + anchors.bottomMargin: contentSpacing + + DankCircularImage { + id: messageIcon + + readonly property string rawImage: modelData?.image || "" + readonly property string iconFromImage: { + if (rawImage.startsWith("image://icon/")) + return rawImage.substring(13); + return ""; + } + readonly property bool imageHasSpecialPrefix: { + const icon = iconFromImage; + return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:"); + } + readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/") + + width: expandedIconSize + height: expandedIconSize + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: Theme.fontSizeSmall * 1.2 + (compactMode ? Theme.spacingXS : Theme.spacingS) + + imageSource: { + if (hasNotificationImage) + return modelData.cleanImage; + if (imageHasSpecialPrefix) + return ""; + const appIcon = modelData?.appIcon; + if (!appIcon) + return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) + return appIcon; + if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) + return ""; + return Paths.resolveIconPath(appIcon); + } + + fallbackIcon: { + if (imageHasSpecialPrefix) + return iconFromImage; + return modelData?.appIcon || iconFromImage || ""; + } + + fallbackText: { + const appName = modelData?.appName || "?"; + return appName.charAt(0).toUpperCase(); + } + } + + Item { + anchors.left: messageIcon.right + anchors.leftMargin: Theme.spacingM + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.top: parent.top + anchors.bottom: parent.bottom + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: buttonArea.top + anchors.bottomMargin: contentSpacing + spacing: Theme.notificationContentSpacing + + Row { + id: expandedDelegateHeaderRow + width: parent.width + spacing: Theme.spacingXS + visible: (expandedDelegateHeaderAppNameText.text.length > 0 || expandedDelegateHeaderTimeText.text.length > 0) + + StyledText { + id: expandedDelegateHeaderAppNameText + text: modelData?.appName || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, parent.width - expandedDelegateHeaderSeparator.implicitWidth - expandedDelegateHeaderTimeText.implicitWidth - parent.spacing * 2) + } + + StyledText { + id: expandedDelegateHeaderSeparator + text: (expandedDelegateHeaderAppNameText.text.length > 0 && expandedDelegateHeaderTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + + StyledText { + id: expandedDelegateHeaderTimeText + text: modelData?.timeStr || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + } + + StyledText { + id: expandedDelegateTitleText + width: parent.width + text: modelData?.summary || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + id: bodyText + property bool hasMoreText: truncated + + text: modelData?.htmlBody || "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: messageExpanded ? Text.ElideNone : Text.ElideRight + maximumLineCount: messageExpanded ? -1 : 2 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { + root.userInitiatedExpansion = true; + NotificationService.toggleMessageExpansion(modelData?.notification?.id || ""); + Qt.callLater(() => { + if (root && !root.isAnimating) + root.userInitiatedExpansion = false; + }); + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false; + } + } + onReleased: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false; + } + } + } + } + } + + Item { + id: buttonArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: actionButtonHeight + contentSpacing + + Row { + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: contentSpacing + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Repeater { + model: modelData?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + height: actionButtonHeight + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + StyledText { + id: expandedActionText + text: { + const baseText = modelData.text || I18n.tr("Open"); + if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) + return `${baseText} (${index + 1})`; + return baseText; + } + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) + modelData.invoke(); + } + } + } + } + + Rectangle { + id: expandedDelegateDismissBtn + property bool isHovered: false + + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 + width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + height: actionButtonHeight + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + StyledText { + id: expandedClearText + text: I18n.tr("Dismiss") + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: NotificationService.dismissNotification(modelData) + } + } + } + } + } + } + } + + DragHandler { + id: expandedSwipeHandler + target: null + xAxis.enabled: true + yAxis.enabled: false + grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType + + onActiveChanged: { + if (active) { + root.swipingNotificationIndex = expandedDelegateWrapper.index; + } else { + root.swipingNotificationIndex = -1; + root.swipingNotificationOffset = 0; + } + if (active || expandedDelegateWrapper.isDismissing) + return; + if (Math.abs(expandedDelegateWrapper.swipeOffset) > expandedDelegateWrapper.dismissThreshold) { + expandedDelegateWrapper.isDismissing = true; + expandedSwipeDismissAnim.start(); + } else { + expandedDelegateWrapper.swipeOffset = 0; + } + } + + onTranslationChanged: { + if (expandedDelegateWrapper.isDismissing) + return; + expandedDelegateWrapper.swipeOffset = translation.x; + root.swipingNotificationOffset = translation.x; + } + } + + NumberAnimation { + id: expandedSwipeDismissAnim + target: expandedDelegateWrapper + property: "swipeOffset" + to: expandedDelegateWrapper.swipeOffset > 0 ? expandedDelegateWrapper.width : -expandedDelegateWrapper.width + duration: Theme.notificationExitDuration + easing.type: Easing.OutCubic + onStopped: NotificationService.dismissNotification(modelData) + } + } + } + } + } + + Row { + visible: !expanded + anchors.right: clearButton.visible ? clearButton.left : parent.right + anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL + anchors.top: collapsedContent.bottom + anchors.topMargin: contentSpacing + collapsedDismissOffset + spacing: contentSpacing + + Repeater { + model: notificationGroup?.latestNotification?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(collapsedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + height: actionButtonHeight + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + StyledText { + id: collapsedActionText + text: { + const baseText = modelData.text || I18n.tr("Open"); + if (keyboardNavigationActive && isGroupSelected) { + return `${baseText} (${index + 1})`; + } + return baseText; + } + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke(); + } + } + } + } + } + } + + Rectangle { + id: clearButton + + property bool isHovered: false + readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length + + visible: !expanded && actionCount < 3 + anchors.right: parent.right + anchors.rightMargin: Theme.spacingL + anchors.top: collapsedContent.bottom + anchors.topMargin: contentSpacing + collapsedDismissOffset + width: Math.max(collapsedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + height: actionButtonHeight + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + StyledText { + id: collapsedClearText + text: I18n.tr("Dismiss") + color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: clearButton.isHovered = true + onExited: clearButton.isHovered = false + onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + MouseArea { + anchors.fill: parent + visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded + cursorShape: Qt.PointingHandCursor + onClicked: { + root.userInitiatedExpansion = true; + NotificationService.toggleGroupExpansion(notificationGroup?.key || ""); + } + z: -1 + } + + Item { + id: fixedControls + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: cardPadding + anchors.rightMargin: Theme.spacingL + width: compactMode ? 52 : 60 + height: compactMode ? 24 : 28 + + DankActionButton { + anchors.left: parent.left + anchors.top: parent.top + visible: (notificationGroup?.count || 0) > 1 + iconName: expanded ? "expand_less" : "expand_more" + iconSize: compactMode ? 16 : 18 + buttonSize: compactMode ? 24 : 28 + onClicked: { + root.userInitiatedExpansion = true; + NotificationService.toggleGroupExpansion(notificationGroup?.key || ""); + } + } + + DankActionButton { + anchors.right: parent.right + anchors.top: parent.top + iconName: "close" + iconSize: compactMode ? 16 : 18 + buttonSize: compactMode ? 24 : 28 + onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + Behavior on height { + enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion + NumberAnimation { + duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.emphasized + onRunningChanged: { + if (running) { + root.isAnimating = true; + } else { + root.isAnimating = false; + root.userInitiatedExpansion = false; + } + } + } + } + + Menu { + id: notificationCardContextMenu + width: 220 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.width: 0 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + MenuItem { + id: setNotificationRulesItem + text: I18n.tr("Set notification rules") + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + const appName = notificationGroup?.appName || ""; + const desktopEntry = notificationGroup?.latestNotification?.desktopEntry || ""; + SettingsData.addNotificationRuleForNotification(appName, desktopEntry); + PopoutService.openSettingsWithTab("notifications"); + } + } + + MenuItem { + id: muteUnmuteItem + readonly property bool isMuted: SettingsData.isAppMuted(notificationGroup?.appName || "", notificationGroup?.latestNotification?.desktopEntry || "") + text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + const appName = notificationGroup?.appName || ""; + const desktopEntry = notificationGroup?.latestNotification?.desktopEntry || ""; + if (isMuted) { + SettingsData.removeMuteRuleForApp(appName, desktopEntry); + } else { + SettingsData.addMuteRuleForApp(appName, desktopEntry); + NotificationService.dismissGroup(notificationGroup?.key || ""); + } + } + } + + MenuItem { + text: I18n.tr("Dismiss") + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + z: -2 + onClicked: mouse => { + if (mouse.button === Qt.RightButton && notificationGroup) { + notificationCardContextMenu.popup(); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml new file mode 100644 index 0000000..b7b616b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -0,0 +1,321 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + layerNamespace: "dms:notification-center-popout" + fullHeightSurface: true + + property bool notificationHistoryVisible: false + property var triggerScreen: null + property real stablePopupHeight: 400 + property real _lastAlignedContentHeight: -1 + property bool _pendingSizedOpen: false + + function updateStablePopupHeight() { + const item = contentLoader.item; + if (item && !root.shouldBeVisible) { + const notificationList = findChild(item, "notificationList"); + if (notificationList && typeof notificationList.forceLayout === "function") { + notificationList.forceLayout(); + } + } + const target = item ? Theme.px(item.implicitHeight, dpr) : 400; + if (Math.abs(target - _lastAlignedContentHeight) < 0.5) + return; + _lastAlignedContentHeight = target; + stablePopupHeight = target; + } + + NotificationKeyboardController { + id: keyboardController + listView: null + isOpen: root.shouldBeVisible + onClose: () => { + notificationHistoryVisible = false; + } + } + + popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 + popupHeight: stablePopupHeight + positioning: "" + animationScaleCollapsed: 0.94 + animationOffset: 0 + suspendShadowWhileResizing: false + + screen: triggerScreen + + function toggle() { + notificationHistoryVisible = !notificationHistoryVisible; + } + + function openSized() { + if (!notificationHistoryVisible) + return; + + primeContent(); + if (contentLoader.item) { + updateStablePopupHeight(); + _pendingSizedOpen = false; + Qt.callLater(() => { + if (!notificationHistoryVisible) + return; + updateStablePopupHeight(); + open(); + clearPrimedContent(); + }); + return; + } + + _pendingSizedOpen = true; + } + + onBackgroundClicked: { + notificationHistoryVisible = false; + } + + onNotificationHistoryVisibleChanged: { + if (notificationHistoryVisible) { + openSized(); + } else { + _pendingSizedOpen = false; + clearPrimedContent(); + close(); + } + } + + function setupKeyboardNavigation() { + if (!contentLoader.item) + return; + contentLoader.item.externalKeyboardController = keyboardController; + + const notificationList = findChild(contentLoader.item, "notificationList"); + const notificationHeader = findChild(contentLoader.item, "notificationHeader"); + + if (notificationList) { + keyboardController.listView = notificationList; + notificationList.keyboardController = keyboardController; + } + if (notificationHeader) { + notificationHeader.keyboardController = keyboardController; + } + + keyboardController.reset(); + keyboardController.rebuildFlatNavigation(); + } + + Connections { + target: contentLoader + function onLoaded() { + root.updateStablePopupHeight(); + if (root._pendingSizedOpen && root.notificationHistoryVisible) { + Qt.callLater(() => { + if (!root._pendingSizedOpen || !root.notificationHistoryVisible) + return; + root.updateStablePopupHeight(); + root._pendingSizedOpen = false; + root.open(); + root.clearPrimedContent(); + }); + return; + } + if (root.shouldBeVisible) + Qt.callLater(root.setupKeyboardNavigation); + } + } + + Connections { + target: contentLoader.item + function onImplicitHeightChanged() { + root.updateStablePopupHeight(); + } + } + + onDprChanged: updateStablePopupHeight() + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + NotificationService.onOverlayOpen(); + updateStablePopupHeight(); + if (contentLoader.item) + Qt.callLater(setupKeyboardNavigation); + } else { + NotificationService.onOverlayClose(); + keyboardController.keyboardNavigationActive = false; + NotificationService.expandedGroups = {}; + NotificationService.expandedMessages = {}; + } + } + + function findChild(parent, objectName) { + if (parent.objectName === objectName) { + return parent; + } + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + const result = findChild(child, objectName); + if (result) { + return result; + } + } + return null; + } + + content: Component { + Rectangle { + id: notificationContent + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var externalKeyboardController: null + property real cachedHeaderHeight: 32 + readonly property real settingsMaxHeight: { + const screenH = root.screen ? root.screen.height : 1080; + const maxPopupH = screenH * 0.8; + const overhead = cachedHeaderHeight + Theme.spacingL * 2 + Theme.spacingM * 2; + return Math.max(200, maxPopupH - overhead - 150); + } + implicitHeight: { + let baseHeight = Theme.spacingL * 2; + baseHeight += cachedHeaderHeight; + baseHeight += Theme.spacingM * 2; + + const settingsHeight = notificationSettings.expanded ? Math.min(notificationSettings.naturalContentHeight, settingsMaxHeight) : 0; + const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight; + let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80); + if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) { + listHeight = 200; + } + if (notificationHeader.currentTab === 1 && NotificationService.historyList.length === 0) { + listHeight = 200; + } + + const maxContentArea = 600; + const availableListSpace = Math.max(200, maxContentArea - settingsHeight); + + baseHeight += settingsHeight; + baseHeight += Math.min(listHeight, availableListSpace); + + const maxHeight = root.screen ? root.screen.height * 0.8 : Screen.height * 0.8; + return Math.max(300, Math.min(baseHeight, maxHeight)); + } + + color: "transparent" + focus: true + + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus(); + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + notificationHistoryVisible = false; + event.accepted = true; + return; + } + + if (event.key === Qt.Key_Left) { + if (notificationHeader.currentTab > 0) { + notificationHeader.currentTab = 0; + event.accepted = true; + } + return; + } + + if (event.key === Qt.Key_Right) { + if (notificationHeader.currentTab === 0 && SettingsData.notificationHistoryEnabled) { + notificationHeader.currentTab = 1; + event.accepted = true; + } + return; + } + if (notificationHeader.currentTab === 1) { + historyList.handleKey(event); + return; + } + if (externalKeyboardController) { + externalKeyboardController.handleKey(event); + } + } + + Connections { + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(() => { + notificationContent.forceActiveFocus(); + }); + } else { + notificationContent.focus = false; + } + } + target: root + } + + FocusScope { + id: contentColumn + + anchors.fill: parent + anchors.margins: Theme.spacingL + focus: true + + Column { + id: contentColumnInner + anchors.fill: parent + spacing: Theme.spacingM + + NotificationHeader { + id: notificationHeader + objectName: "notificationHeader" + onHeightChanged: notificationContent.cachedHeaderHeight = height + } + + NotificationSettings { + id: notificationSettings + expanded: notificationHeader.showSettings + maxAllowedHeight: notificationContent.settingsMaxHeight + } + + Item { + visible: notificationHeader.currentTab === 0 + width: parent.width + height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2 + + KeyboardNavigatedNotificationList { + id: notificationList + objectName: "notificationList" + anchors.fill: parent + anchors.leftMargin: -shadowHorizontalGutter + anchors.rightMargin: -shadowHorizontalGutter + anchors.topMargin: -(shadowVerticalGutter + delegateShadowGutter / 2) + anchors.bottomMargin: -(shadowVerticalGutter + delegateShadowGutter / 2) + cardAnimateExpansion: true + } + } + + HistoryNotificationList { + id: historyList + visible: notificationHeader.currentTab === 1 + width: parent.width + height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2 + } + } + } + + NotificationKeyboardHints { + id: keyboardHints + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: notificationHeader.currentTab === 0 ? (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false : historyList.showKeyboardHints + z: 200 + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml new file mode 100644 index 0000000..da8746e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml @@ -0,0 +1,34 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + width: parent.width + height: 200 + visible: NotificationService.notifications.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + width: parent.width * 0.8 + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "notifications_none" + size: Theme.iconSizeLarge + 16 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr("Nothing to see here") + font.pixelSize: Theme.fontSizeLarge + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + font.weight: Font.Medium + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationHeader.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationHeader.qml new file mode 100644 index 0000000..201e716 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationHeader.qml @@ -0,0 +1,158 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var keyboardController: null + property bool showSettings: false + property int currentTab: 0 + + onCurrentTabChanged: { + if (currentTab === 1 && !SettingsData.notificationHistoryEnabled) + currentTab = 0; + } + + Connections { + target: SettingsData + function onNotificationHistoryEnabledChanged() { + if (!SettingsData.notificationHistoryEnabled) + root.currentTab = 0; + } + } + + width: parent.width + height: headerColumn.implicitHeight + + DankTooltipV2 { + id: sharedTooltip + } + + Column { + id: headerColumn + width: parent.width + spacing: Theme.spacingS + + Item { + width: parent.width + height: Math.max(titleRow.implicitHeight, actionsRow.implicitHeight) + + Row { + id: titleRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Notifications") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + id: doNotDisturbButton + iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications" + iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText + buttonSize: Theme.iconSize + Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + onEntered: sharedTooltip.show(I18n.tr("Do Not Disturb"), doNotDisturbButton, 0, 0, "bottom") + onExited: sharedTooltip.hide() + } + } + + Row { + id: actionsRow + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + id: helpButton + iconName: "info" + iconColor: (keyboardController && keyboardController.showKeyboardHints) ? Theme.primary : Theme.surfaceText + buttonSize: Theme.iconSize + Theme.spacingS + visible: keyboardController !== null + anchors.verticalCenter: parent.verticalCenter + onClicked: { + if (keyboardController) + keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints; + } + } + + DankActionButton { + id: settingsButton + iconName: "settings" + iconColor: root.showSettings ? Theme.primary : Theme.surfaceText + buttonSize: Theme.iconSize + Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + onClicked: root.showSettings = !root.showSettings + } + + Rectangle { + id: clearAllButton + width: clearButtonContent.implicitWidth + Theme.spacingM * 2 + height: Theme.iconSize + Theme.spacingS + radius: Theme.cornerRadius + visible: root.currentTab === 0 ? NotificationService.notifications.length > 0 : NotificationService.historyList.length > 0 + color: clearArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + + Row { + id: clearButtonContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "delete_sweep" + size: Theme.iconSizeSmall + color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Clear") + font.pixelSize: Theme.fontSizeSmall + color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: clearArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.currentTab === 0) { + NotificationService.clearAllNotifications(); + } else { + NotificationService.clearHistory(); + } + } + } + } + } + } + + DankButtonGroup { + id: tabGroup + width: parent.width + currentIndex: root.currentTab + buttonHeight: 32 + buttonPadding: Theme.spacingM + checkEnabled: false + textSize: Theme.fontSizeSmall + visible: SettingsData.notificationHistoryEnabled + model: [I18n.tr("Current", "notification center tab") + " (" + NotificationService.notifications.length + ")", I18n.tr("History", "notification center tab") + " (" + NotificationService.historyList.length + ")"] + onSelectionChanged: (index, selected) => { + if (selected) + root.currentTab = index; + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml new file mode 100644 index 0000000..eda12c0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml @@ -0,0 +1,556 @@ +import QtQuick +import qs.Services + +QtObject { + id: controller + + property var listView: null + property bool isOpen: false + property var onClose: null + + property int selectionVersion: 0 + + property bool keyboardNavigationActive: false + property int selectedFlatIndex: 0 + property var flatNavigation: [] + property bool showKeyboardHints: false + + property string selectedNotificationId: "" + property string selectedGroupKey: "" + property string selectedItemType: "" + property bool isTogglingGroup: false + property bool isRebuilding: false + + function rebuildFlatNavigation() { + isRebuilding = true; + + const nav = []; + const groups = NotificationService.groupedNotifications; + + for (var i = 0; i < groups.length; i++) { + const group = groups[i]; + const isExpanded = NotificationService.expandedGroups[group.key] || false; + + nav.push({ + "type": "group", + "groupIndex": i, + "notificationIndex": -1, + "groupKey": group.key, + "notificationId": "" + }); + + if (isExpanded) { + const notifications = group.notifications || []; + const maxNotifications = Math.min(notifications.length, 10); + for (var j = 0; j < maxNotifications; j++) { + const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : ""); + nav.push({ + "type": "notification", + "groupIndex": i, + "notificationIndex": j, + "groupKey": group.key, + "notificationId": notifId + }); + } + } + } + + flatNavigation = nav; + updateSelectedIndexFromId(); + isRebuilding = false; + } + + function updateSelectedIndexFromId() { + if (!keyboardNavigationActive) { + return; + } + + for (var i = 0; i < flatNavigation.length; i++) { + const item = flatNavigation[i]; + + if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) { + selectedFlatIndex = i; + selectionVersion++; // Trigger UI update + return; + } else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) { + selectedFlatIndex = i; + selectionVersion++; // Trigger UI update + return; + } + } + + // If not found, try to find the same group but select the group header instead + if (selectedItemType === "notification") { + for (var j = 0; j < flatNavigation.length; j++) { + const groupItem = flatNavigation[j]; + if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) { + selectedFlatIndex = j; + selectedItemType = "group"; + selectedNotificationId = ""; + selectionVersion++; // Trigger UI update + return; + } + } + } + + // If still not found, clamp to valid range and update + if (flatNavigation.length > 0) { + selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1); + selectedFlatIndex = Math.max(selectedFlatIndex, 0); + updateSelectedIdFromIndex(); + selectionVersion++; // Trigger UI update + } + } + + function updateSelectedIdFromIndex() { + if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) { + const item = flatNavigation[selectedFlatIndex]; + selectedItemType = item.type; + selectedGroupKey = item.groupKey; + selectedNotificationId = item.notificationId; + } + } + + function reset() { + selectedFlatIndex = 0; + keyboardNavigationActive = false; + showKeyboardHints = false; + // Reset keyboardActive when modal is reset + if (listView) { + listView.keyboardActive = false; + } + rebuildFlatNavigation(); + } + + function selectNext() { + keyboardNavigationActive = true; + if (flatNavigation.length === 0) + return; + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll(); + } + + selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1); + updateSelectedIdFromIndex(); + selectionVersion++; + ensureVisible(); + } + + function selectNextWrapping() { + keyboardNavigationActive = true; + if (flatNavigation.length === 0) + return; + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll(); + } + + selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length; + updateSelectedIdFromIndex(); + selectionVersion++; + ensureVisible(); + } + + function selectPrevious() { + keyboardNavigationActive = true; + if (flatNavigation.length === 0) + return; + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll(); + } + + selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0); + updateSelectedIdFromIndex(); + selectionVersion++; + ensureVisible(); + } + + function toggleGroupExpanded() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + const groups = NotificationService.groupedNotifications; + const group = groups[currentItem.groupIndex]; + if (!group) + return; + + // Prevent expanding groups with < 2 notifications + const notificationCount = group.notifications ? group.notifications.length : 0; + if (notificationCount < 2) + return; + const wasExpanded = NotificationService.expandedGroups[group.key] || false; + const groupIndex = currentItem.groupIndex; + + isTogglingGroup = true; + NotificationService.toggleGroupExpansion(group.key); + rebuildFlatNavigation(); + + // Smart selection after toggle + if (!wasExpanded) { + // Just expanded - move to first notification in the group + for (var i = 0; i < flatNavigation.length; i++) { + if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) { + selectedFlatIndex = i; + break; + } + } + } else { + // Just collapsed - stay on the group header + for (var i = 0; i < flatNavigation.length; i++) { + if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) { + selectedFlatIndex = i; + break; + } + } + } + + isTogglingGroup = false; + } + + function handleEnterKey() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + const groups = NotificationService.groupedNotifications; + const group = groups[currentItem.groupIndex]; + if (!group) + return; + if (currentItem.type === "group") { + const notificationCount = group.notifications ? group.notifications.length : 0; + if (notificationCount >= 2) { + toggleGroupExpanded(); + } else { + executeAction(0); + } + } else if (currentItem.type === "notification") { + executeAction(0); + } + } + + function toggleTextExpanded() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + const groups = NotificationService.groupedNotifications; + const group = groups[currentItem.groupIndex]; + if (!group) + return; + let messageId = ""; + + if (currentItem.type === "group") { + messageId = group.latestNotification?.notification?.id + "_desc"; + } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { + messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc"; + } + + if (messageId) { + NotificationService.toggleMessageExpansion(messageId); + } + } + + function executeAction(actionIndex) { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + const groups = NotificationService.groupedNotifications; + const group = groups[currentItem.groupIndex]; + if (!group) + return; + let actions = []; + + if (currentItem.type === "group") { + actions = group.latestNotification?.actions || []; + } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { + actions = group.notifications[currentItem.notificationIndex]?.actions || []; + } + + if (actionIndex >= 0 && actionIndex < actions.length) { + const action = actions[actionIndex]; + if (action.invoke) { + action.invoke(); + if (onClose) + onClose(); + } + } + } + + function clearSelected() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + const groups = NotificationService.groupedNotifications; + const group = groups[currentItem.groupIndex]; + if (!group) + return; + if (currentItem.type === "group") { + NotificationService.dismissGroup(group.key); + } else if (currentItem.type === "notification") { + const notification = group.notifications[currentItem.notificationIndex]; + NotificationService.dismissNotification(notification); + } + + rebuildFlatNavigation(); + + if (flatNavigation.length === 0) { + keyboardNavigationActive = false; + if (listView) { + listView.keyboardActive = false; + } + } else { + selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1); + updateSelectedIdFromIndex(); + ensureVisible(); + } + } + + function findRepeater(parent) { + if (!parent || !parent.children) { + return null; + } + for (var i = 0; i < parent.children.length; i++) { + const child = parent.children[i]; + if (child.objectName === "notificationRepeater") { + return child; + } + const found = findRepeater(child); + if (found) { + return found; + } + } + return null; + } + + function ensureVisible() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView) + return; + const currentItem = flatNavigation[selectedFlatIndex]; + + if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) { + if (currentItem.type === "notification") { + const groupDelegate = listView.itemAtIndex(currentItem.groupIndex); + if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) { + const notificationCard = groupDelegate.children[0]; + const repeater = findRepeater(notificationCard); + + if (repeater && currentItem.notificationIndex < repeater.count) { + const notificationItem = repeater.itemAt(currentItem.notificationIndex); + if (notificationItem) { + const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0); + const itemY = itemPos.y; + const itemHeight = notificationItem.height; + + const viewportTop = listView.contentY; + const viewportBottom = listView.contentY + listView.height; + + if (itemY < viewportTop) { + listView.contentY = itemY - 20; + } else if (itemY + itemHeight > viewportBottom) { + listView.contentY = itemY + itemHeight - listView.height + 20; + } + } + } + } + } else { + listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain); + } + + listView.forceLayout(); + } + } + + function handleKey(event) { + if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) { + NotificationService.clearAllNotifications(); + rebuildFlatNavigation(); + if (flatNavigation.length === 0) { + keyboardNavigationActive = false; + if (listView) { + listView.keyboardActive = false; + } + } else { + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + } + selectionVersion++; + event.accepted = true; + return; + } + + if (event.key === Qt.Key_Escape) { + if (keyboardNavigationActive) { + keyboardNavigationActive = false; + event.accepted = true; + } else { + if (onClose) + onClose(); + event.accepted = true; + } + } else if (event.key === Qt.Key_Down || event.key === 16777237) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); // Ensure we have fresh navigation data + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + // Set keyboardActive on listView to show highlight + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + event.accepted = true; + } else { + selectNext(); + event.accepted = true; + } + } else if (event.key === Qt.Key_Up || event.key === 16777235) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); // Ensure we have fresh navigation data + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + // Set keyboardActive on listView to show highlight + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + event.accepted = true; + } else if (selectedFlatIndex === 0) { + keyboardNavigationActive = false; + // Reset keyboardActive when navigation is disabled + if (listView) { + listView.keyboardActive = false; + } + selectionVersion++; + event.accepted = true; + return; + } else { + selectPrevious(); + event.accepted = true; + } + } else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + } else { + selectNext(); + } + event.accepted = true; + } else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + } else if (selectedFlatIndex === 0) { + keyboardNavigationActive = false; + if (listView) { + listView.keyboardActive = false; + } + selectionVersion++; + } else { + selectPrevious(); + } + event.accepted = true; + } else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + } else { + selectNext(); + } + event.accepted = true; + } else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true; + rebuildFlatNavigation(); + selectedFlatIndex = 0; + updateSelectedIdFromIndex(); + if (listView) { + listView.keyboardActive = true; + } + selectionVersion++; + ensureVisible(); + } else if (selectedFlatIndex === 0) { + keyboardNavigationActive = false; + if (listView) { + listView.keyboardActive = false; + } + selectionVersion++; + } else { + selectPrevious(); + } + event.accepted = true; + } else if (keyboardNavigationActive) { + if (event.key === Qt.Key_Space) { + toggleGroupExpanded(); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + handleEnterKey(); + event.accepted = true; + } else if (event.key === Qt.Key_E) { + toggleTextExpanded(); + event.accepted = true; + } else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { + clearSelected(); + event.accepted = true; + } else if (event.key === Qt.Key_Tab) { + selectNext(); + event.accepted = true; + } else if (event.key === Qt.Key_Backtab) { + selectPrevious(); + event.accepted = true; + } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { + const actionIndex = event.key - Qt.Key_1; + executeAction(actionIndex); + event.accepted = true; + } + } + + if (event.key === Qt.Key_F10) { + showKeyboardHints = !showKeyboardHints; + event.accepted = true; + } + } + + // Get current selection info for UI + function getCurrentSelection() { + if (!keyboardNavigationActive || selectedFlatIndex < 0 || selectedFlatIndex >= flatNavigation.length) { + return { + "type": "", + "groupIndex": -1, + "notificationIndex": -1 + }; + } + const result = flatNavigation[selectedFlatIndex] || { + "type": "", + "groupIndex": -1, + "notificationIndex": -1 + }; + return result; + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml new file mode 100644 index 0000000..78ba2d5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml @@ -0,0 +1,50 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showHints: false + + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: showHints ? 1 : 0 + z: 100 + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingS + spacing: 2 + + StyledText { + text: I18n.tr("↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: I18n.tr("Del: Clear • Shift+Del: Clear All • 1-9: Actions • F10: Help • Esc: Close") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationSettings.qml new file mode 100644 index 0000000..97364c7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationSettings.qml @@ -0,0 +1,449 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool expanded: false + property real maxAllowedHeight: 0 + readonly property real naturalContentHeight: contentColumn.height + Theme.spacingL * 2 + + width: parent.width + height: expanded ? (maxAllowedHeight > 0 ? Math.min(naturalContentHeight, maxAllowedHeight) : naturalContentHeight) : 0 + visible: expanded + clip: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + border.width: 1 + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + opacity: expanded ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + readonly property var timeoutOptions: [ + { + "text": I18n.tr("Never"), + "value": 0 + }, + { + "text": I18n.tr("1 second"), + "value": 1000 + }, + { + "text": I18n.tr("3 seconds"), + "value": 3000 + }, + { + "text": I18n.tr("5 seconds"), + "value": 5000 + }, + { + "text": I18n.tr("8 seconds"), + "value": 8000 + }, + { + "text": I18n.tr("10 seconds"), + "value": 10000 + }, + { + "text": I18n.tr("15 seconds"), + "value": 15000 + }, + { + "text": I18n.tr("30 seconds"), + "value": 30000 + }, + { + "text": I18n.tr("1 minute"), + "value": 60000 + }, + { + "text": I18n.tr("2 minutes"), + "value": 120000 + }, + { + "text": I18n.tr("5 minutes"), + "value": 300000 + }, + { + "text": I18n.tr("10 minutes"), + "value": 600000 + } + ] + + function getTimeoutText(value) { + if (value === undefined || value === null || isNaN(value)) { + return I18n.tr("5 seconds"); + } + + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].value === value) { + return timeoutOptions[i].text; + } + } + if (value === 0) { + return I18n.tr("Never"); + } + if (value < 1000) { + return value + "ms"; + } + if (value < 60000) { + return Math.round(value / 1000) + " " + I18n.tr("seconds"); + } + return Math.round(value / 60000) + " " + I18n.tr("minutes"); + } + + Flickable { + id: settingsFlickable + anchors.fill: parent + contentHeight: contentColumn.height + Theme.spacingL * 2 + clip: true + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.DragAndOvershootBounds + interactive: root.naturalContentHeight > root.height + + Column { + id: contentColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Notification Settings") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + } + + Item { + width: parent.width + height: Math.max(dndRow.implicitHeight, dndToggle.implicitHeight) + Theme.spacingS + + Row { + id: dndRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: SessionData.doNotDisturb ? "notifications_off" : "notifications" + size: Theme.iconSizeSmall + color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Do Not Disturb") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: dndToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SessionData.doNotDisturb + onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + StyledText { + text: I18n.tr("Notification Timeouts") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + } + + DankDropdown { + text: I18n.tr("Low Priority") + description: I18n.tr("Timeout for low priority notifications") + currentValue: getTimeoutText(SettingsData.notificationTimeoutLow) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value); + break; + } + } + } + } + + DankDropdown { + text: I18n.tr("Normal Priority") + description: I18n.tr("Timeout for normal priority notifications") + currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value); + break; + } + } + } + } + + DankDropdown { + text: I18n.tr("Critical Priority") + description: I18n.tr("Timeout for critical priority notifications") + currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value); + break; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + Item { + width: parent.width + height: Math.max(overlayRow.implicitHeight, overlayToggle.implicitHeight) + Theme.spacingS + + Row { + id: overlayRow + anchors.left: parent.left + anchors.right: overlayToggle.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "notifications_active" + size: Theme.iconSizeSmall + color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: overlayRow.width - Theme.iconSizeSmall - Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Notification Overlay") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + wrapMode: Text.Wrap + } + + StyledText { + width: parent.width + text: I18n.tr("Display all priorities over fullscreen apps") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + wrapMode: Text.Wrap + } + } + } + + DankToggle { + id: overlayToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationOverlayEnabled + onToggled: toggled => SettingsData.set("notificationOverlayEnabled", toggled) + } + } + + Item { + width: parent.width + height: Math.max(privacyRow.implicitHeight, privacyToggle.implicitHeight) + Theme.spacingS + + Row { + id: privacyRow + anchors.left: parent.left + anchors.right: privacyToggle.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "privacy_tip" + size: Theme.iconSizeSmall + color: SettingsData.notificationPopupPrivacyMode ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: privacyRow.width - Theme.iconSizeSmall - Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Privacy Mode") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + wrapMode: Text.Wrap + } + + StyledText { + width: parent.width + text: I18n.tr("Hide notification content until expanded") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + wrapMode: Text.Wrap + } + } + } + + DankToggle { + id: privacyToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationPopupPrivacyMode + onToggled: toggled => SettingsData.set("notificationPopupPrivacyMode", toggled) + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + StyledText { + text: I18n.tr("History Settings") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + } + + Item { + width: parent.width + height: Math.max(lowRow.implicitHeight, lowToggle.implicitHeight) + Theme.spacingS + + Row { + id: lowRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "low_priority" + size: Theme.iconSizeSmall + color: SettingsData.notificationHistorySaveLow ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Low Priority") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: lowToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationHistorySaveLow + onToggled: toggled => SettingsData.set("notificationHistorySaveLow", toggled) + } + } + + Item { + width: parent.width + height: Math.max(normalRow.implicitHeight, normalToggle.implicitHeight) + Theme.spacingS + + Row { + id: normalRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "notifications" + size: Theme.iconSizeSmall + color: SettingsData.notificationHistorySaveNormal ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Normal Priority") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: normalToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationHistorySaveNormal + onToggled: toggled => SettingsData.set("notificationHistorySaveNormal", toggled) + } + } + + Item { + width: parent.width + height: Math.max(criticalRow.implicitHeight, criticalToggle.implicitHeight) + Theme.spacingS + + Row { + id: criticalRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "priority_high" + size: Theme.iconSizeSmall + color: SettingsData.notificationHistorySaveCritical ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Critical Priority") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: criticalToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationHistorySaveCritical + onToggled: toggled => SettingsData.set("notificationHistorySaveCritical", toggled) + } + } + } + } +} |