mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-26 14:32:52 -05:00
motifications: add support for configurable persistent history
fixes #929
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notifications.Center
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
@@ -112,8 +111,11 @@ DankPopout {
|
||||
baseHeight += Theme.spacingM * 2;
|
||||
|
||||
const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0;
|
||||
let listHeight = notificationList.listContentHeight;
|
||||
if (NotificationService.groupedNotifications.length === 0) {
|
||||
let listHeight = notificationHeader.currentTab === 0 ? notificationList.listContentHeight : 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;
|
||||
}
|
||||
|
||||
@@ -143,7 +145,13 @@ DankPopout {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
notificationHistoryVisible = false;
|
||||
event.accepted = true;
|
||||
} else if (externalKeyboardController) {
|
||||
return;
|
||||
}
|
||||
if (notificationHeader.currentTab === 1) {
|
||||
historyList.handleKey(event);
|
||||
return;
|
||||
}
|
||||
if (externalKeyboardController) {
|
||||
externalKeyboardController.handleKey(event);
|
||||
}
|
||||
}
|
||||
@@ -187,7 +195,14 @@ DankPopout {
|
||||
KeyboardNavigatedNotificationList {
|
||||
id: 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
|
||||
height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2
|
||||
}
|
||||
@@ -200,7 +215,7 @@ DankPopout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false
|
||||
showHints: notificationHeader.currentTab === 0 ? (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false : historyList.showKeyboardHints
|
||||
z: 200
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,108 +8,150 @@ Item {
|
||||
|
||||
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: 32
|
||||
height: headerColumn.implicitHeight
|
||||
|
||||
DankTooltipV2 {
|
||||
id: sharedTooltip
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
Column {
|
||||
id: headerColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
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: 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)
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(titleRow.implicitHeight, actionsRow.implicitHeight)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
id: titleRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
Row {
|
||||
id: actionsRow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NotificationService.clearAllNotifications()
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
@@ -36,64 +34,77 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var timeoutOptions: [{
|
||||
readonly property var timeoutOptions: [
|
||||
{
|
||||
"text": "Never",
|
||||
"value": 0
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "1 second",
|
||||
"value": 1000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "3 seconds",
|
||||
"value": 3000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "5 seconds",
|
||||
"value": 5000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "8 seconds",
|
||||
"value": 8000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "10 seconds",
|
||||
"value": 10000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "15 seconds",
|
||||
"value": 15000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "30 seconds",
|
||||
"value": 30000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "1 minute",
|
||||
"value": 60000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "2 minutes",
|
||||
"value": 120000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "5 minutes",
|
||||
"value": 300000
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"text": "10 minutes",
|
||||
"value": 600000
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
function getTimeoutText(value) {
|
||||
if (value === undefined || value === null || isNaN(value)) {
|
||||
return "5 seconds"
|
||||
return "5 seconds";
|
||||
}
|
||||
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].value === value) {
|
||||
return timeoutOptions[i].text
|
||||
return timeoutOptions[i].text;
|
||||
}
|
||||
}
|
||||
if (value === 0) {
|
||||
return "Never"
|
||||
return "Never";
|
||||
}
|
||||
if (value < 1000) {
|
||||
return value + "ms"
|
||||
return value + "ms";
|
||||
}
|
||||
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 {
|
||||
@@ -113,9 +124,10 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
height: Math.max(dndRow.implicitHeight, dndToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: dndRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
@@ -136,6 +148,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: dndToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SessionData.doNotDisturb
|
||||
@@ -162,13 +175,13 @@ Rectangle {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
@@ -177,13 +190,13 @@ Rectangle {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
@@ -192,13 +205,13 @@ Rectangle {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -209,9 +222,10 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
height: Math.max(overlayRow.implicitHeight, overlayToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: overlayRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
@@ -242,11 +256,127 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: overlayToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationOverlayEnabled
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user