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:
@@ -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
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
quickshell/Widgets/DankFilterChips.qml
Normal file
101
quickshell/Widgets/DankFilterChips.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user