1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

motifications: add support for configurable persistent history

fixes #929
This commit is contained in:
bbedward
2026-01-03 13:08:48 -05:00
parent faddc46185
commit 2bf85bc4dd
11 changed files with 1233 additions and 140 deletions

View File

@@ -351,6 +351,12 @@ Singleton {
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50
property int notificationHistoryMaxAgeDays: 7
property bool notificationHistorySaveLow: true
property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter

View File

@@ -240,6 +240,12 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationHistoryEnabled: { def: true },
notificationHistoryMaxCount: { def: 50 },
notificationHistoryMaxAgeDays: { def: 7 },
notificationHistorySaveLow: { def: true },
notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },

View File

@@ -18,6 +18,8 @@ DankModal {
property bool notificationModalOpen: false property bool notificationModalOpen: false
property var notificationListRef: null property var notificationListRef: null
property var historyListRef: null
property int currentTab: 0
function show() { function show() {
notificationModalOpen = true; notificationModalOpen = true;
@@ -61,7 +63,7 @@ DankModal {
NotificationService.clearAllNotifications(); NotificationService.clearAllNotifications();
} }
function dismissAllPopups () { function dismissAllPopups() {
NotificationService.dismissAllPopups(); NotificationService.dismissAllPopups();
} }
@@ -80,7 +82,18 @@ DankModal {
NotificationService.onOverlayClose(); NotificationService.onOverlayClose();
} }
} }
modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event) modalFocusScope.Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
if (currentTab === 1 && historyListRef) {
historyListRef.handleKey(event);
return;
}
modalKeyboardController.handleKey(event);
}
NotificationKeyboardController { NotificationKeyboardController {
id: modalKeyboardController id: modalKeyboardController
@@ -145,21 +158,20 @@ DankModal {
NotificationHeader { NotificationHeader {
id: notificationHeader id: notificationHeader
keyboardController: modalKeyboardController keyboardController: modalKeyboardController
onCurrentTabChanged: notificationModal.currentTab = currentTab
} }
NotificationSettings { NotificationSettings {
id: notificationSettings id: notificationSettings
expanded: notificationHeader.showSettings expanded: notificationHeader.showSettings
} }
KeyboardNavigatedNotificationList { KeyboardNavigatedNotificationList {
id: notificationList id: notificationList
width: parent.width width: parent.width
height: parent.height - y height: parent.height - y
visible: notificationHeader.currentTab === 0
keyboardController: modalKeyboardController keyboardController: modalKeyboardController
Component.onCompleted: { Component.onCompleted: {
notificationModal.notificationListRef = notificationList; notificationModal.notificationListRef = notificationList;
@@ -169,6 +181,14 @@ DankModal {
} }
} }
} }
HistoryNotificationList {
id: historyList
width: parent.width
height: parent.height - y
visible: notificationHeader.currentTab === 1
Component.onCompleted: notificationModal.historyListRef = historyList
}
} }
NotificationKeyboardHints { NotificationKeyboardHints {
@@ -178,7 +198,7 @@ DankModal {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints showHints: notificationHeader.currentTab === 0 ? modalKeyboardController.showKeyboardHints : historyList.showKeyboardHints
} }
} }
} }

View File

@@ -0,0 +1,189 @@
import QtQuick
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
width: parent ? parent.width : 400
height: 116
radius: Theme.cornerRadius
clip: true
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 1;
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
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 {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
anchors.rightMargin: 56
height: 92
DankCircularImage {
id: iconContainer
readonly property bool hasNotificationImage: historyItem.image && historyItem.image !== ""
width: 63
height: 63
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 14
imageSource: {
if (hasNotificationImage)
return historyItem.image;
if (historyItem.appIcon) {
const appIcon = historyItem.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon;
return Quickshell.iconPath(appIcon, true);
}
return "";
}
hasImage: hasNotificationImage
fallbackIcon: ""
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: 12
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
color: "transparent"
Item {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: -2
Column {
width: parent.width
spacing: 2
StyledText {
width: parent.width
text: {
const timeStr = NotificationService.formatHistoryTime(historyItem.timestamp);
const appName = historyItem.appName || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: historyItem.summary || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: descriptionText
text: historyItem.htmlBody || historyItem.body || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => Qt.openUrlExternally(link)
}
}
}
}
}
DankActionButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 12
anchors.rightMargin: 16
iconName: "close"
iconSize: 18
buttonSize: 28
onClicked: NotificationService.removeFromHistory(historyItem.id)
}
}

View File

@@ -0,0 +1,276 @@
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() {
}
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
model: ScriptModel {
id: historyModel
values: root.getFilteredHistory()
objectProp: "id"
}
NotificationEmptyState {
visible: historyListView.count === 0
y: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
}
delegate: HistoryNotificationCard {
required property var modelData
required property int index
width: ListView.view.width
historyItem: modelData
isSelected: root.keyboardActive && root.selectedIndex === index
keyboardNavigationActive: root.keyboardActive
}
}
}
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;
}
}
}

View File

@@ -2,7 +2,6 @@ import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.Notifications.Center
DankPopout { DankPopout {
id: root id: root
@@ -112,8 +111,11 @@ DankPopout {
baseHeight += Theme.spacingM * 2; baseHeight += Theme.spacingM * 2;
const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0; const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0;
let listHeight = notificationList.listContentHeight; let listHeight = notificationHeader.currentTab === 0 ? notificationList.listContentHeight : Math.max(200, NotificationService.historyList.length * 80);
if (NotificationService.groupedNotifications.length === 0) { if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
listHeight = 200;
}
if (notificationHeader.currentTab === 1 && NotificationService.historyList.length === 0) {
listHeight = 200; listHeight = 200;
} }
@@ -143,7 +145,13 @@ DankPopout {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
notificationHistoryVisible = false; notificationHistoryVisible = false;
event.accepted = true; event.accepted = true;
} else if (externalKeyboardController) { return;
}
if (notificationHeader.currentTab === 1) {
historyList.handleKey(event);
return;
}
if (externalKeyboardController) {
externalKeyboardController.handleKey(event); externalKeyboardController.handleKey(event);
} }
} }
@@ -187,7 +195,14 @@ DankPopout {
KeyboardNavigatedNotificationList { KeyboardNavigatedNotificationList {
id: notificationList id: notificationList
objectName: "notificationList" objectName: "notificationList"
visible: notificationHeader.currentTab === 0
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2
}
HistoryNotificationList {
id: historyList
visible: notificationHeader.currentTab === 1
width: parent.width width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2 height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2
} }
@@ -200,7 +215,7 @@ DankPopout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
showHints: (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false showHints: notificationHeader.currentTab === 0 ? (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false : historyList.showKeyboardHints
z: 200 z: 200
} }
} }

View File

@@ -8,108 +8,150 @@ Item {
property var keyboardController: null property var keyboardController: null
property bool showSettings: false 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 width: parent.width
height: 32 height: headerColumn.implicitHeight
DankTooltipV2 { DankTooltipV2 {
id: sharedTooltip id: sharedTooltip
} }
Row { Column {
anchors.left: parent.left id: headerColumn
anchors.verticalCenter: parent.verticalCenter width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingS
StyledText { Item {
text: I18n.tr("Notifications") width: parent.width
font.pixelSize: Theme.fontSizeLarge height: Math.max(titleRow.implicitHeight, actionsRow.implicitHeight)
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: 28
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 {
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: 28
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: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: root.showSettings = !root.showSettings
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadius
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Row { Row {
anchors.centerIn: parent id: titleRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon {
name: "delete_sweep"
size: Theme.iconSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText { StyledText {
text: I18n.tr("Clear") text: I18n.tr("Notifications")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeLarge
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter 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()
}
} }
MouseArea { Row {
id: clearArea id: actionsRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
anchors.fill: parent DankActionButton {
hoverEnabled: true id: helpButton
cursorShape: Qt.PointingHandCursor iconName: "info"
onClicked: NotificationService.clearAllNotifications() 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;
} }
} }
} }

View File

@@ -1,7 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
@@ -36,64 +34,77 @@ Rectangle {
} }
} }
readonly property var timeoutOptions: [{ readonly property var timeoutOptions: [
{
"text": "Never", "text": "Never",
"value": 0 "value": 0
}, { },
{
"text": "1 second", "text": "1 second",
"value": 1000 "value": 1000
}, { },
{
"text": "3 seconds", "text": "3 seconds",
"value": 3000 "value": 3000
}, { },
{
"text": "5 seconds", "text": "5 seconds",
"value": 5000 "value": 5000
}, { },
{
"text": "8 seconds", "text": "8 seconds",
"value": 8000 "value": 8000
}, { },
{
"text": "10 seconds", "text": "10 seconds",
"value": 10000 "value": 10000
}, { },
{
"text": "15 seconds", "text": "15 seconds",
"value": 15000 "value": 15000
}, { },
{
"text": "30 seconds", "text": "30 seconds",
"value": 30000 "value": 30000
}, { },
{
"text": "1 minute", "text": "1 minute",
"value": 60000 "value": 60000
}, { },
{
"text": "2 minutes", "text": "2 minutes",
"value": 120000 "value": 120000
}, { },
{
"text": "5 minutes", "text": "5 minutes",
"value": 300000 "value": 300000
}, { },
{
"text": "10 minutes", "text": "10 minutes",
"value": 600000 "value": 600000
}] }
]
function getTimeoutText(value) { function getTimeoutText(value) {
if (value === undefined || value === null || isNaN(value)) { if (value === undefined || value === null || isNaN(value)) {
return "5 seconds" return "5 seconds";
} }
for (let i = 0; i < timeoutOptions.length; i++) { for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].value === value) { if (timeoutOptions[i].value === value) {
return timeoutOptions[i].text return timeoutOptions[i].text;
} }
} }
if (value === 0) { if (value === 0) {
return "Never" return "Never";
} }
if (value < 1000) { if (value < 1000) {
return value + "ms" return value + "ms";
} }
if (value < 60000) { if (value < 60000) {
return Math.round(value / 1000) + " seconds" return Math.round(value / 1000) + " seconds";
} }
return Math.round(value / 60000) + " minutes" return Math.round(value / 60000) + " minutes";
} }
Column { Column {
@@ -113,9 +124,10 @@ Rectangle {
Item { Item {
width: parent.width width: parent.width
height: 36 height: Math.max(dndRow.implicitHeight, dndToggle.implicitHeight) + Theme.spacingS
Row { Row {
id: dndRow
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -136,6 +148,7 @@ Rectangle {
} }
DankToggle { DankToggle {
id: dndToggle
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SessionData.doNotDisturb checked: SessionData.doNotDisturb
@@ -162,13 +175,13 @@ Rectangle {
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow) currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value) SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value);
break break;
} }
} }
} }
} }
DankDropdown { DankDropdown {
@@ -177,13 +190,13 @@ Rectangle {
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal) currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value) SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value);
break break;
} }
} }
} }
} }
DankDropdown { DankDropdown {
@@ -192,13 +205,13 @@ Rectangle {
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical) currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value) SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value);
break break;
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -209,9 +222,10 @@ Rectangle {
Item { Item {
width: parent.width width: parent.width
height: 36 height: Math.max(overlayRow.implicitHeight, overlayToggle.implicitHeight) + Theme.spacingS
Row { Row {
id: overlayRow
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -242,11 +256,127 @@ Rectangle {
} }
DankToggle { DankToggle {
id: overlayToggle
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.notificationOverlayEnabled checked: SettingsData.notificationOverlayEnabled
onToggled: toggled => SettingsData.set("notificationOverlayEnabled", toggled) onToggled: toggled => SettingsData.set("notificationOverlayEnabled", 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)
}
}
} }
} }

View File

@@ -219,6 +219,105 @@ Item {
} }
} }
} }
SettingsCard {
width: parent.width
iconName: "history"
title: I18n.tr("History Settings")
settingKey: "notificationHistory"
SettingsToggleRow {
settingKey: "notificationHistoryEnabled"
tags: ["notification", "history", "enable", "disable", "save"]
text: I18n.tr("Enable History", "notification history toggle label")
description: I18n.tr("Save dismissed notifications to history", "notification history toggle description")
checked: SettingsData.notificationHistoryEnabled
onToggled: checked => SettingsData.set("notificationHistoryEnabled", checked)
}
SettingsSliderRow {
settingKey: "notificationHistoryMaxCount"
tags: ["notification", "history", "max", "count", "limit"]
text: I18n.tr("Maximum History")
description: I18n.tr("Maximum number of notifications to keep", "notification history limit")
value: SettingsData.notificationHistoryMaxCount
minimum: 10
maximum: 200
step: 10
unit: ""
defaultValue: 50
onSliderValueChanged: newValue => SettingsData.set("notificationHistoryMaxCount", newValue)
}
SettingsDropdownRow {
settingKey: "notificationHistoryMaxAgeDays"
tags: ["notification", "history", "max", "age", "days", "retention"]
text: I18n.tr("History Retention", "notification history retention settings label")
description: I18n.tr("Auto-delete notifications older than this", "notification history setting")
currentValue: {
switch (SettingsData.notificationHistoryMaxAgeDays) {
case 0:
return I18n.tr("Forever", "notification history retention option");
case 1:
return I18n.tr("1 day", "notification history retention option");
case 3:
return I18n.tr("3 days", "notification history retention option");
case 7:
return I18n.tr("7 days", "notification history retention option");
case 14:
return I18n.tr("14 days", "notification history retention option");
case 30:
return I18n.tr("30 days", "notification history retention option");
default:
return SettingsData.notificationHistoryMaxAgeDays + " " + I18n.tr("days");
}
}
options: [I18n.tr("Forever", "notification history retention option"), I18n.tr("1 day", "notification history retention option"), I18n.tr("3 days", "notification history retention option"), I18n.tr("7 days", "notification history retention option"), I18n.tr("14 days", "notification history retention option"), I18n.tr("30 days", "notification history retention option")]
onValueChanged: value => {
let days = 7;
if (value === I18n.tr("Forever", "notification history retention option"))
days = 0;
else if (value === I18n.tr("1 day", "notification history retention option"))
days = 1;
else if (value === I18n.tr("3 days", "notification history retention option"))
days = 3;
else if (value === I18n.tr("7 days", "notification history retention option"))
days = 7;
else if (value === I18n.tr("14 days", "notification history retention option"))
days = 14;
else if (value === I18n.tr("30 days", "notification history retention option"))
days = 30;
SettingsData.set("notificationHistoryMaxAgeDays", days);
}
}
SettingsToggleRow {
settingKey: "notificationHistorySaveLow"
tags: ["notification", "history", "save", "low", "priority"]
text: I18n.tr("Low Priority")
description: I18n.tr("Save low priority notifications to history", "notification history setting")
checked: SettingsData.notificationHistorySaveLow
onToggled: checked => SettingsData.set("notificationHistorySaveLow", checked)
}
SettingsToggleRow {
settingKey: "notificationHistorySaveNormal"
tags: ["notification", "history", "save", "normal", "priority"]
text: I18n.tr("Normal Priority")
description: I18n.tr("Save normal priority notifications to history", "notification history setting")
checked: SettingsData.notificationHistorySaveNormal
onToggled: checked => SettingsData.set("notificationHistorySaveNormal", checked)
}
SettingsToggleRow {
settingKey: "notificationHistorySaveCritical"
tags: ["notification", "history", "save", "critical", "priority"]
text: I18n.tr("Critical Priority")
description: I18n.tr("Save critical priority notifications to history", "notification history setting")
checked: SettingsData.notificationHistorySaveCritical
onToggled: checked => SettingsData.set("notificationHistorySaveCritical", checked)
}
}
} }
} }
} }

View File

@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import qs.Common import qs.Common
import "../Common/markdown2html.js" as Markdown2Html import "../Common/markdown2html.js" as Markdown2Html
@@ -14,6 +15,10 @@ Singleton {
readonly property list<NotifWrapper> allWrappers: [] readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n && n.popup) readonly property list<NotifWrapper> popups: allWrappers.filter(n => n && n.popup)
property var historyList: []
readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json"
property bool historyLoaded: false
property list<NotifWrapper> notificationQueue: [] property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: [] property list<NotifWrapper> visibleNotifications: []
property int maxVisibleNotifications: 3 property int maxVisibleNotifications: 3
@@ -26,7 +31,7 @@ Singleton {
property int maxIngressPerSecond: 20 property int maxIngressPerSecond: 20
property double _lastIngressSec: 0 property double _lastIngressSec: 0
property int _ingressCountThisSec: 0 property int _ingressCountThisSec: 0
property int maxStoredNotifications: 50 property int maxStoredNotifications: SettingsData.notificationHistoryMaxCount
property var _dismissQueue: [] property var _dismissQueue: []
property int _dismissBatchSize: 8 property int _dismissBatchSize: 8
@@ -40,6 +45,165 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
_recomputeGroups(); _recomputeGroups();
Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]);
}
FileView {
id: historyFileView
path: root.historyFile
printErrors: false
onLoaded: root.loadHistory()
onLoadFailed: error => {
if (error === 2) {
root.historyLoaded = true;
historyFileView.writeAdapter();
}
}
JsonAdapter {
id: historyAdapter
property var notifications: []
}
}
Timer {
id: historySaveTimer
interval: 200
onTriggered: root.performSaveHistory()
}
function addToHistory(wrapper) {
if (!wrapper)
return;
const urg = typeof wrapper.urgency === "number" ? wrapper.urgency : 1;
const data = {
id: wrapper.notification?.id?.toString() || Date.now().toString(),
summary: wrapper.summary || "",
body: wrapper.body || "",
htmlBody: wrapper.htmlBody || wrapper.body || "",
appName: wrapper.appName || "",
appIcon: wrapper.appIcon || "",
image: wrapper.cleanImage || "",
urgency: urg,
timestamp: wrapper.time.getTime(),
desktopEntry: wrapper.desktopEntry || ""
};
let newList = [data, ...historyList];
if (newList.length > SettingsData.notificationHistoryMaxCount) {
newList = newList.slice(0, SettingsData.notificationHistoryMaxCount);
}
historyList = newList;
saveHistory();
}
function saveHistory() {
historySaveTimer.restart();
}
function performSaveHistory() {
try {
historyAdapter.notifications = historyList;
historyFileView.writeAdapter();
} catch (e) {
console.warn("NotificationService: save history failed:", e);
}
}
function loadHistory() {
try {
const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays;
const now = Date.now();
const maxAgeMs = maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : 0;
const loaded = [];
for (const item of historyAdapter.notifications || []) {
if (maxAgeMs > 0 && (now - item.timestamp) > maxAgeMs)
continue;
const urg = typeof item.urgency === "number" ? item.urgency : 1;
const body = item.body || "";
let htmlBody = item.htmlBody || "";
if (!htmlBody && body) {
htmlBody = (body.includes('<') && body.includes('>')) ? body : Markdown2Html.markdownToHtml(body);
}
loaded.push({
id: item.id || "",
summary: item.summary || "",
body: body,
htmlBody: htmlBody,
appName: item.appName || "",
appIcon: item.appIcon || "",
image: item.image || "",
urgency: urg,
timestamp: item.timestamp || 0,
desktopEntry: item.desktopEntry || ""
});
}
historyList = loaded;
historyLoaded = true;
if (maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length)
saveHistory();
} catch (e) {
console.warn("NotificationService: load history failed:", e);
historyLoaded = true;
}
}
function removeFromHistory(notificationId) {
const idx = historyList.findIndex(n => n.id === notificationId);
if (idx >= 0) {
historyList = historyList.filter((_, i) => i !== idx);
saveHistory();
return true;
}
return false;
}
function clearHistory() {
historyList = [];
saveHistory();
}
function getHistoryTimeRange(timestamp) {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const itemDate = new Date(timestamp);
const itemDay = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate());
const diffDays = Math.floor((today - itemDay) / (1000 * 60 * 60 * 24));
if (diffDays === 0)
return 0;
if (diffDays === 1)
return 1;
return 2;
}
function getHistoryCountForRange(range) {
if (range === -1)
return historyList.length;
return historyList.filter(n => getHistoryTimeRange(n.timestamp) === range).length;
}
function formatHistoryTime(timestamp) {
root.timeUpdateTick;
root.clockFormatChanged;
const now = new Date();
const date = new Date(timestamp);
const diff = now.getTime() - timestamp;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
if (hours < 1) {
if (minutes < 1)
return I18n.tr("now");
return I18n.tr("%1m ago").arg(minutes);
}
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const daysDiff = Math.floor((nowDate - itemDate) / (1000 * 60 * 60 * 24));
const timeStr = SettingsData.use24HourClock ? date.toLocaleTimeString(Qt.locale(), "HH:mm") : date.toLocaleTimeString(Qt.locale(), "h:mm AP");
if (daysDiff === 0)
return timeStr;
if (daysDiff === 1)
return I18n.tr("yesterday") + ", " + timeStr;
return I18n.tr("%1 days ago").arg(daysDiff);
} }
function _nowSec() { function _nowSec() {
@@ -84,6 +248,40 @@ Singleton {
wrapper.isPersistent = isCritical || (timeoutMs === 0); wrapper.isPersistent = isCritical || (timeoutMs === 0);
} }
function _shouldSaveToHistory(urgency) {
if (!SettingsData.notificationHistoryEnabled)
return false;
switch (urgency) {
case NotificationUrgency.Low:
return SettingsData.notificationHistorySaveLow;
case NotificationUrgency.Critical:
return SettingsData.notificationHistorySaveCritical;
default:
return SettingsData.notificationHistorySaveNormal;
}
}
function pruneHistory() {
const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays;
if (maxAgeDays <= 0)
return;
const now = Date.now();
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const pruned = historyList.filter(item => (now - item.timestamp) <= maxAgeMs);
if (pruned.length !== historyList.length) {
historyList = pruned;
saveHistory();
}
}
function deleteHistory() {
historyList = [];
historyAdapter.notifications = [];
historyFileView.writeAdapter();
}
function _trimStored() { function _trimStored() {
if (notifications.length > maxStoredNotifications) { if (notifications.length > maxStoredNotifications) {
const overflow = notifications.length - maxStoredNotifications; const overflow = notifications.length - maxStoredNotifications;
@@ -121,6 +319,7 @@ Singleton {
} }
visibleNotifications = []; visibleNotifications = [];
_recomputeGroupsLater(); _recomputeGroupsLater();
pruneHistory();
} }
function onOverlayClose() { function onOverlayClose() {
@@ -234,9 +433,11 @@ Singleton {
if (wrapper) { if (wrapper) {
root.allWrappers.push(wrapper); root.allWrappers.push(wrapper);
if (!isTransient) { const shouldSave = !isTransient && _shouldSaveToHistory(notif.urgency);
if (shouldSave) {
root.notifications.push(wrapper); root.notifications.push(wrapper);
_trimStored(); _trimStored();
root.addToHistory(wrapper);
} }
Qt.callLater(() => { Qt.callLater(() => {
@@ -703,5 +904,13 @@ Singleton {
function onUse24HourClockChanged() { function onUse24HourClockChanged() {
root.clockFormatChanged = !root.clockFormatChanged; root.clockFormatChanged = !root.clockFormatChanged;
} }
function onNotificationHistoryMaxAgeDaysChanged() {
root.pruneHistory();
}
function onNotificationHistoryEnabledChanged() {
if (!SettingsData.notificationHistoryEnabled) {
root.deleteHistory();
}
}
} }
} }

View File

@@ -0,0 +1,101 @@
import QtQuick
import qs.Common
import qs.Widgets
Flow {
id: root
property var model: []
property int currentIndex: 0
property int chipHeight: 32
property int chipPadding: Theme.spacingM
property bool showCheck: true
property bool showCounts: true
signal selectionChanged(int index)
spacing: Theme.spacingS
width: parent ? parent.width : 400
Repeater {
model: root.model
Rectangle {
id: chip
required property var modelData
required property int index
property bool selected: index === root.currentIndex
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
property int count: typeof modelData === "object" ? (modelData.count || 0) : 0
property bool showCount: root.showCounts && count > 0
width: contentRow.implicitWidth + root.chipPadding * 2
height: root.chipHeight
radius: height / 2
color: selected ? Theme.primary : Theme.surfaceVariant
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: {
if (pressed)
return chip.selected ? Theme.primaryPressed : Theme.surfaceTextHover;
if (hovered)
return chip.selected ? Theme.primaryHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "check"
size: 16
anchors.verticalCenter: parent.verticalCenter
color: Theme.primaryText
visible: root.showCheck && chip.selected
}
StyledText {
text: chip.label + (chip.showCount ? " (" + chip.count + ")" : "")
font.pixelSize: Theme.fontSizeSmall
font.weight: chip.selected ? Font.Medium : Font.Normal
color: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentIndex = chip.index;
root.selectionChanged(chip.index);
}
}
}
}
}