mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge upstream/main into pr/networking-refactor-pt2 and fix BluetoothSubTab.qml conflict
This commit is contained in:
@@ -43,7 +43,7 @@ NIconButton {
|
||||
function computeUnreadCount() {
|
||||
var since = NotificationService.lastSeenTs;
|
||||
var count = 0;
|
||||
var model = NotificationService.historyList;
|
||||
var model = NotificationService.historyModel;
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i);
|
||||
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp;
|
||||
@@ -71,8 +71,8 @@ NIconButton {
|
||||
colorFg: Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
visible: !((hideWhenZero && NotificationService.historyList.count === 0) || (hideWhenZeroUnread && count === 0))
|
||||
opacity: !((hideWhenZero && NotificationService.historyList.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0
|
||||
visible: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0))
|
||||
opacity: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
@@ -26,7 +26,7 @@ Variants {
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
property ListModel notificationModel: NotificationService.activeList
|
||||
property ListModel notificationModel: NotificationService.popupModel
|
||||
|
||||
// Deferred activation to prevent re-entrant QML incubation crash.
|
||||
// Direct binding to notificationModel.count would activate the Loader
|
||||
@@ -177,7 +177,7 @@ Variants {
|
||||
}
|
||||
} catch (e) {
|
||||
// Service fallback if delegate is already invalid
|
||||
NotificationService.dismissActiveNotification(notificationId);
|
||||
NotificationService.dismissPopup(notificationId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -369,7 +369,7 @@ Variants {
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.dismissActiveNotification(notificationId);
|
||||
NotificationService.dismissPopup(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function moveSelection(dir) {
|
||||
var m = NotificationService.historyList;
|
||||
var m = NotificationService.historyModel;
|
||||
if (!m || m.count === 0)
|
||||
return;
|
||||
|
||||
@@ -139,7 +139,7 @@ SmartPanel {
|
||||
function moveAction(dir) {
|
||||
if (focusIndex === -1)
|
||||
return;
|
||||
var item = NotificationService.historyList.get(focusIndex);
|
||||
var item = NotificationService.historyModel.get(focusIndex);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
@@ -162,7 +162,7 @@ SmartPanel {
|
||||
function activateSelection() {
|
||||
if (focusIndex === -1)
|
||||
return;
|
||||
var item = NotificationService.historyList.get(focusIndex);
|
||||
var item = NotificationService.historyModel.get(focusIndex);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
@@ -189,7 +189,7 @@ SmartPanel {
|
||||
function removeSelection() {
|
||||
if (focusIndex === -1)
|
||||
return;
|
||||
var item = NotificationService.historyList.get(focusIndex);
|
||||
var item = NotificationService.historyModel.get(focusIndex);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
@@ -237,7 +237,7 @@ SmartPanel {
|
||||
|
||||
// Calculate content height based on header + tabs (if visible) + content
|
||||
property real calculatedHeight: {
|
||||
if (NotificationService.historyList.count === 0) {
|
||||
if (NotificationService.historyModel.count === 0) {
|
||||
return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM;
|
||||
}
|
||||
return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM;
|
||||
@@ -296,7 +296,7 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function recalcRangeCounts() {
|
||||
var m = NotificationService.historyList;
|
||||
var m = NotificationService.historyModel;
|
||||
if (!m || typeof m.count === "undefined" || m.count <= 0) {
|
||||
panelContent.rangeCounts = [0, 0, 0, 0];
|
||||
return;
|
||||
@@ -328,7 +328,7 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function hasNotificationsInCurrentRange() {
|
||||
var m = NotificationService.historyList;
|
||||
var m = NotificationService.historyModel;
|
||||
if (!m || m.count === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -347,7 +347,7 @@ SmartPanel {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NotificationService.historyList
|
||||
target: NotificationService.historyModel
|
||||
function onCountChanged() {
|
||||
panelContent.recalcRangeCounts();
|
||||
}
|
||||
@@ -433,7 +433,7 @@ SmartPanel {
|
||||
NTabBar {
|
||||
id: tabsBox
|
||||
Layout.fillWidth: true
|
||||
visible: NotificationService.historyList.count > 0 && panelContent.groupByDate
|
||||
visible: NotificationService.historyModel.count > 0 && panelContent.groupByDate
|
||||
currentIndex: panelContent.currentRange
|
||||
tabHeight: Style.toOdd(Style.baseWidgetSize * 0.8)
|
||||
spacing: Style.marginXS
|
||||
@@ -512,20 +512,20 @@ SmartPanel {
|
||||
|
||||
NIcon {
|
||||
icon: "bell-off"
|
||||
pointSize: (NotificationService.historyList.count === 0) ? 48 : Style.baseWidgetSize
|
||||
pointSize: (NotificationService.historyModel.count === 0) ? 48 : Style.baseWidgetSize
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("notifications.panel.no-notifications")
|
||||
pointSize: (NotificationService.historyList.count === 0) ? Style.fontSizeL : Style.fontSizeM
|
||||
pointSize: (NotificationService.historyModel.count === 0) ? Style.fontSizeL : Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: NotificationService.historyList.count === 0
|
||||
visible: NotificationService.historyModel.count === 0
|
||||
text: I18n.tr("notifications.panel.description")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
@@ -552,7 +552,7 @@ SmartPanel {
|
||||
spacing: Style.marginM
|
||||
|
||||
Repeater {
|
||||
model: NotificationService.historyList
|
||||
model: NotificationService.historyModel
|
||||
|
||||
delegate: Item {
|
||||
id: notificationDelegate
|
||||
|
||||
@@ -658,19 +658,20 @@ Item {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
Layout.topMargin: -Style.marginXXS
|
||||
Layout.row: detailsGrid ? 2 : 5
|
||||
Layout.column: detailsGrid ? 1 : 0
|
||||
spacing: Style.marginXS
|
||||
visible: Settings.data.network.bluetoothAutoConnect
|
||||
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceAutoConnect(modelData.address) ? "repeat" : "repeat-off"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: BluetoothService.getDeviceAutoConnect(modelData.address) ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
NCheckbox {
|
||||
label: I18n.tr("common.auto-connect")
|
||||
labelSize: Style.fontSizeXS
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
baseSize: Style.baseWidgetSize * 0.5
|
||||
checked: BluetoothService.getDeviceAutoConnect(modelData.address)
|
||||
onToggled: checked => BluetoothService.setDeviceAutoConnect(modelData, checked)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var addMonitor
|
||||
|
||||
@@ -37,11 +37,11 @@ Singleton {
|
||||
Logger.w("IPC", "Argument to ipc call '" + funcName + "' must be a number");
|
||||
return null;
|
||||
}
|
||||
if (idx < 0 || idx >= NotificationService.activeList.count) {
|
||||
if (idx < 0 || idx >= NotificationService.popupModel.count) {
|
||||
Logger.w("IPC", "Notification index out of range: " + idx);
|
||||
return null;
|
||||
}
|
||||
return NotificationService.activeList.get(idx);
|
||||
return NotificationService.popupModel.get(idx);
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
@@ -191,7 +191,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function dismissOldest() {
|
||||
NotificationService.dismissOldestActive();
|
||||
NotificationService.dismissOldestPopup();
|
||||
}
|
||||
|
||||
function removeOldestHistory() {
|
||||
@@ -199,7 +199,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function dismissAll() {
|
||||
NotificationService.dismissAllActive();
|
||||
NotificationService.dismissAllPopups();
|
||||
}
|
||||
|
||||
function getHistory(): string {
|
||||
@@ -230,13 +230,13 @@ Singleton {
|
||||
|
||||
var actions = JSON.parse(notif.actionsJson || "[]");
|
||||
if (actions.length === 0) {
|
||||
NotificationService.dismissActiveNotification(notif.id);
|
||||
NotificationService.dismissPopup(notif.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier;
|
||||
var result = NotificationService.invokeAction(notif.id, actionId);
|
||||
NotificationService.dismissActiveNotification(notif.id);
|
||||
NotificationService.dismissPopup(notif.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
// Configuration
|
||||
property int maxVisible: 5
|
||||
property int maxPopups: 5
|
||||
property int maxHistory: 100
|
||||
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
|
||||
|
||||
@@ -27,11 +27,11 @@ Singleton {
|
||||
property bool doNotDisturb: false
|
||||
|
||||
// Models
|
||||
property ListModel activeList: ListModel {}
|
||||
property ListModel historyList: ListModel {}
|
||||
property ListModel popupModel: ListModel {}
|
||||
property ListModel historyModel: ListModel {}
|
||||
|
||||
// Internal state
|
||||
property var activeNotifications: ({}) // Maps internal ID to {notification, watcher, metadata}
|
||||
property var popupState: ({}) // Maps internal ID to {notification, watcher, cachedActions, metadata}
|
||||
property var quickshellIdToInternalId: ({})
|
||||
|
||||
// Rate limiting for notification sounds (minimum 100ms between sounds)
|
||||
@@ -167,19 +167,19 @@ Singleton {
|
||||
|
||||
// Check if this is a replacement notification
|
||||
const existingInternalId = quickshellIdToInternalId[quickshellId];
|
||||
if (existingInternalId && activeNotifications[existingInternalId]) {
|
||||
updateExistingNotification(existingInternalId, notification, data);
|
||||
if (existingInternalId && popupState[existingInternalId]) {
|
||||
updatePopup(existingInternalId, notification, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate content
|
||||
const duplicateId = findDuplicateNotification(data);
|
||||
if (duplicateId) {
|
||||
removeNotification(duplicateId);
|
||||
removePopup(duplicateId);
|
||||
}
|
||||
|
||||
// Add new notification
|
||||
addNewNotification(quickshellId, notification, data);
|
||||
addPopup(quickshellId, notification, data);
|
||||
playNotificationSound(data.urgency, notification.appName);
|
||||
}
|
||||
|
||||
@@ -277,30 +277,30 @@ Singleton {
|
||||
return defaultSoundFile;
|
||||
}
|
||||
|
||||
function updateExistingNotification(internalId, notification, data) {
|
||||
const index = findNotificationIndex(internalId);
|
||||
function updatePopup(internalId, notification, data) {
|
||||
const index = findPopupIndex(internalId);
|
||||
if (index < 0)
|
||||
return;
|
||||
const existing = activeList.get(index);
|
||||
const existing = popupModel.get(index);
|
||||
const oldTimestamp = existing.timestamp;
|
||||
const oldProgress = existing.progress;
|
||||
|
||||
// Update properties (keeping original timestamp and progress)
|
||||
activeList.setProperty(index, "summary", data.summary);
|
||||
activeList.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
|
||||
activeList.setProperty(index, "body", data.body);
|
||||
activeList.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
|
||||
activeList.setProperty(index, "appName", data.appName);
|
||||
activeList.setProperty(index, "urgency", data.urgency);
|
||||
activeList.setProperty(index, "expireTimeout", data.expireTimeout);
|
||||
activeList.setProperty(index, "originalImage", data.originalImage);
|
||||
activeList.setProperty(index, "cachedImage", data.cachedImage);
|
||||
activeList.setProperty(index, "actionsJson", data.actionsJson);
|
||||
activeList.setProperty(index, "timestamp", oldTimestamp);
|
||||
activeList.setProperty(index, "progress", oldProgress);
|
||||
popupModel.setProperty(index, "summary", data.summary);
|
||||
popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
|
||||
popupModel.setProperty(index, "body", data.body);
|
||||
popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
|
||||
popupModel.setProperty(index, "appName", data.appName);
|
||||
popupModel.setProperty(index, "urgency", data.urgency);
|
||||
popupModel.setProperty(index, "expireTimeout", data.expireTimeout);
|
||||
popupModel.setProperty(index, "originalImage", data.originalImage);
|
||||
popupModel.setProperty(index, "cachedImage", data.cachedImage);
|
||||
popupModel.setProperty(index, "actionsJson", data.actionsJson);
|
||||
popupModel.setProperty(index, "timestamp", oldTimestamp);
|
||||
popupModel.setProperty(index, "progress", oldProgress);
|
||||
|
||||
// Update stored notification object
|
||||
const notifData = activeNotifications[internalId];
|
||||
const notifData = popupState[internalId];
|
||||
notifData.notification = notification;
|
||||
|
||||
// Deep copy actions to preserve them even if QML object clears list
|
||||
@@ -319,7 +319,7 @@ Singleton {
|
||||
notification.tracked = true;
|
||||
|
||||
function onClosed() {
|
||||
userDismissNotification(internalId);
|
||||
dismissPopup(internalId);
|
||||
}
|
||||
notification.closed.connect(onClosed);
|
||||
notifData.onClosed = onClosed;
|
||||
@@ -329,7 +329,7 @@ Singleton {
|
||||
notifData.metadata.duration = calculateDuration(data);
|
||||
}
|
||||
|
||||
function addNewNotification(quickshellId, notification, data) {
|
||||
function addPopup(quickshellId, notification, data) {
|
||||
// Map IDs
|
||||
quickshellIdToInternalId[quickshellId] = data.id;
|
||||
|
||||
@@ -351,7 +351,7 @@ Singleton {
|
||||
}
|
||||
|
||||
// Store notification data
|
||||
activeNotifications[data.id] = {
|
||||
popupState[data.id] = {
|
||||
"notification": notification,
|
||||
"watcher": watcher,
|
||||
"cachedActions": safeActions // Cache actions
|
||||
@@ -370,29 +370,34 @@ Singleton {
|
||||
notification.tracked = true;
|
||||
|
||||
function onClosed() {
|
||||
userDismissNotification(data.id);
|
||||
dismissPopup(data.id);
|
||||
}
|
||||
notification.closed.connect(onClosed);
|
||||
activeNotifications[data.id].onClosed = onClosed;
|
||||
popupState[data.id].onClosed = onClosed;
|
||||
|
||||
// Add to list
|
||||
activeList.insert(0, data);
|
||||
// Defer list insertion to prevent re-entrant QML incubation crash.
|
||||
// Direct insert triggers Repeater.modelUpdated synchronously, which
|
||||
// incubates delegates whose signal handlers can re-enter the V4 engine
|
||||
// and crash in QV4::Object::insertMember.
|
||||
Qt.callLater(() => {
|
||||
popupModel.insert(0, data);
|
||||
|
||||
// Remove overflow
|
||||
while (activeList.count > maxVisible) {
|
||||
const last = activeList.get(activeList.count - 1);
|
||||
// Overflow only removes from ACTIVE view, but keeps it for history
|
||||
activeNotifications[last.id]?.notification?.dismiss(); // Visually dismiss
|
||||
activeList.remove(activeList.count - 1);
|
||||
// DO NOT call cleanupNotification here, we want to keep it for history actions
|
||||
}
|
||||
// Remove overflow
|
||||
while (popupModel.count > maxPopups) {
|
||||
const last = popupModel.get(popupModel.count - 1);
|
||||
// Overflow only removes from ACTIVE view, but keeps it for history
|
||||
popupState[last.id]?.notification?.dismiss(); // Visually dismiss
|
||||
popupModel.remove(popupModel.count - 1);
|
||||
// DO NOT call cleanupNotification here, we want to keep it for history actions
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findDuplicateNotification(data) {
|
||||
const contentId = getContentId(data.summary, data.body, data.appName);
|
||||
|
||||
for (var i = 0; i < activeList.count; i++) {
|
||||
const existing = activeList.get(i);
|
||||
for (var i = 0; i < popupModel.count; i++) {
|
||||
const existing = popupModel.get(i);
|
||||
const existingContentId = getContentId(existing.summary, existing.body, existing.appName);
|
||||
if (existingContentId === contentId) {
|
||||
return existing.id;
|
||||
@@ -450,9 +455,9 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
function findNotificationIndex(internalId) {
|
||||
for (var i = 0; i < activeList.count; i++) {
|
||||
if (activeList.get(i).id === internalId) {
|
||||
function findPopupIndex(internalId) {
|
||||
for (var i = 0; i < popupModel.count; i++) {
|
||||
if (popupModel.get(i).id === internalId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -460,45 +465,45 @@ Singleton {
|
||||
}
|
||||
|
||||
function updateNotificationFromObject(internalId) {
|
||||
const notifData = activeNotifications[internalId];
|
||||
const notifData = popupState[internalId];
|
||||
if (!notifData)
|
||||
return;
|
||||
const index = findNotificationIndex(internalId);
|
||||
const index = findPopupIndex(internalId);
|
||||
if (index < 0)
|
||||
return;
|
||||
const data = createData(notifData.notification);
|
||||
const existing = activeList.get(index);
|
||||
const existing = popupModel.get(index);
|
||||
|
||||
// Update properties (keeping timestamp and progress)
|
||||
activeList.setProperty(index, "summary", data.summary);
|
||||
activeList.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
|
||||
activeList.setProperty(index, "body", data.body);
|
||||
activeList.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
|
||||
activeList.setProperty(index, "appName", data.appName);
|
||||
activeList.setProperty(index, "urgency", data.urgency);
|
||||
activeList.setProperty(index, "expireTimeout", data.expireTimeout);
|
||||
activeList.setProperty(index, "originalImage", data.originalImage);
|
||||
activeList.setProperty(index, "cachedImage", data.cachedImage);
|
||||
activeList.setProperty(index, "actionsJson", data.actionsJson);
|
||||
popupModel.setProperty(index, "summary", data.summary);
|
||||
popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
|
||||
popupModel.setProperty(index, "body", data.body);
|
||||
popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
|
||||
popupModel.setProperty(index, "appName", data.appName);
|
||||
popupModel.setProperty(index, "urgency", data.urgency);
|
||||
popupModel.setProperty(index, "expireTimeout", data.expireTimeout);
|
||||
popupModel.setProperty(index, "originalImage", data.originalImage);
|
||||
popupModel.setProperty(index, "cachedImage", data.cachedImage);
|
||||
popupModel.setProperty(index, "actionsJson", data.actionsJson);
|
||||
|
||||
// Update metadata
|
||||
notifData.metadata.urgency = data.urgency;
|
||||
notifData.metadata.duration = calculateDuration(data);
|
||||
}
|
||||
|
||||
function removeNotification(id) {
|
||||
const index = findNotificationIndex(id);
|
||||
function removePopup(id) {
|
||||
const index = findPopupIndex(id);
|
||||
if (index >= 0) {
|
||||
activeList.remove(index);
|
||||
popupModel.remove(index);
|
||||
}
|
||||
cleanupNotification(id);
|
||||
}
|
||||
|
||||
function cleanupNotification(id) {
|
||||
const notifData = activeNotifications[id];
|
||||
const notifData = popupState[id];
|
||||
if (notifData) {
|
||||
notifData.watcher?.destroy();
|
||||
delete activeNotifications[id];
|
||||
delete popupState[id];
|
||||
}
|
||||
|
||||
// Clean up quickshell ID mapping
|
||||
@@ -514,7 +519,7 @@ Singleton {
|
||||
Timer {
|
||||
interval: 50
|
||||
repeat: true
|
||||
running: activeList.count > 0
|
||||
running: popupModel.count > 0
|
||||
onTriggered: updateAllProgress()
|
||||
}
|
||||
|
||||
@@ -522,9 +527,9 @@ Singleton {
|
||||
const now = Date.now();
|
||||
const toRemove = [];
|
||||
|
||||
for (var i = 0; i < activeList.count; i++) {
|
||||
const notif = activeList.get(i);
|
||||
const notifData = activeNotifications[notif.id];
|
||||
for (var i = 0; i < popupModel.count; i++) {
|
||||
const notif = popupModel.get(i);
|
||||
const notifData = popupState[notif.id];
|
||||
if (!notifData)
|
||||
continue;
|
||||
const meta = notifData.metadata;
|
||||
@@ -536,7 +541,7 @@ Singleton {
|
||||
if (progress <= 0) {
|
||||
toRemove.push(notif.id);
|
||||
} else if (Math.abs(notif.progress - progress) > 0.005) {
|
||||
activeList.setProperty(i, "progress", progress);
|
||||
popupModel.setProperty(i, "progress", progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,8 +571,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function updateImagePath(notificationId, path) {
|
||||
updateModel(activeList, notificationId, "cachedImage", path);
|
||||
updateModel(historyList, notificationId, "cachedImage", path);
|
||||
updateModel(popupModel, notificationId, "cachedImage", path);
|
||||
updateModel(historyModel, notificationId, "cachedImage", path);
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
@@ -582,18 +587,22 @@ Singleton {
|
||||
|
||||
// History management
|
||||
function addToHistory(data) {
|
||||
historyList.insert(0, data);
|
||||
// Defer list insertion to prevent re-entrant QML incubation crash.
|
||||
// See addPopup for full explanation.
|
||||
Qt.callLater(() => {
|
||||
historyModel.insert(0, data);
|
||||
|
||||
while (historyList.count > maxHistory) {
|
||||
const old = historyList.get(historyList.count - 1);
|
||||
// Only delete cached images that are in our cache directory
|
||||
const cachedPath = old.cachedImage ? old.cachedImage.replace(/^file:\/\//, "") : "";
|
||||
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
|
||||
Quickshell.execDetached(["rm", "-f", cachedPath]);
|
||||
}
|
||||
historyList.remove(historyList.count - 1);
|
||||
}
|
||||
saveHistory();
|
||||
while (historyModel.count > maxHistory) {
|
||||
const old = historyModel.get(historyModel.count - 1);
|
||||
// Only delete cached images that are in our cache directory
|
||||
const cachedPath = old.cachedImage ? old.cachedImage.replace(/^file:\/\//, "") : "";
|
||||
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
|
||||
Quickshell.execDetached(["rm", "-f", cachedPath]);
|
||||
}
|
||||
historyModel.remove(historyModel.count - 1);
|
||||
}
|
||||
saveHistory();
|
||||
});
|
||||
}
|
||||
|
||||
// Persistence - History
|
||||
@@ -626,8 +635,8 @@ Singleton {
|
||||
function performSaveHistory() {
|
||||
try {
|
||||
const items = [];
|
||||
for (var i = 0; i < historyList.count; i++) {
|
||||
const n = historyList.get(i);
|
||||
for (var i = 0; i < historyModel.count; i++) {
|
||||
const n = historyModel.get(i);
|
||||
const copy = Object.assign({}, n);
|
||||
copy.timestamp = n.timestamp.getTime();
|
||||
items.push(copy);
|
||||
@@ -641,7 +650,7 @@ Singleton {
|
||||
|
||||
function loadHistory() {
|
||||
try {
|
||||
historyList.clear();
|
||||
historyModel.clear();
|
||||
for (const item of adapter.notifications || []) {
|
||||
const time = new Date(item.timestamp);
|
||||
|
||||
@@ -651,20 +660,20 @@ Singleton {
|
||||
cachedImage = item.originalImage || "";
|
||||
}
|
||||
|
||||
historyList.append({
|
||||
"id": item.id || "",
|
||||
"summary": item.summary || "",
|
||||
"summaryMarkdown": processNotificationMarkdown(item.summary || ""),
|
||||
"body": item.body || "",
|
||||
"bodyMarkdown": processNotificationMarkdown(item.body || ""),
|
||||
"appName": item.appName || "",
|
||||
"urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
|
||||
"timestamp": time,
|
||||
"originalImage": item.originalImage || "",
|
||||
"cachedImage": cachedImage,
|
||||
"actionsJson": item.actionsJson || "[]",
|
||||
"originalId": item.originalId || 0
|
||||
});
|
||||
historyModel.append({
|
||||
"id": item.id || "",
|
||||
"summary": item.summary || "",
|
||||
"summaryMarkdown": processNotificationMarkdown(item.summary || ""),
|
||||
"body": item.body || "",
|
||||
"bodyMarkdown": processNotificationMarkdown(item.body || ""),
|
||||
"appName": item.appName || "",
|
||||
"urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
|
||||
"timestamp": time,
|
||||
"originalImage": item.originalImage || "",
|
||||
"cachedImage": cachedImage,
|
||||
"actionsJson": item.actionsJson || "[]",
|
||||
"originalId": item.originalId || 0
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("Notifications", "Load failed:", e);
|
||||
@@ -861,7 +870,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function pauseTimeout(id) {
|
||||
const notifData = activeNotifications[id];
|
||||
const notifData = popupState[id];
|
||||
if (notifData && !notifData.metadata.paused) {
|
||||
notifData.metadata.paused = true;
|
||||
notifData.metadata.pauseTime = Date.now();
|
||||
@@ -869,46 +878,41 @@ Singleton {
|
||||
}
|
||||
|
||||
function resumeTimeout(id) {
|
||||
const notifData = activeNotifications[id];
|
||||
const notifData = popupState[id];
|
||||
if (notifData && notifData.metadata.paused) {
|
||||
notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime;
|
||||
notifData.metadata.paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
function dismissActiveNotification(id) {
|
||||
userDismissNotification(id);
|
||||
}
|
||||
|
||||
// User dismissed from active view (e.g. clicked close, or swipe)
|
||||
// This behaves like "overflow" - removes from active list but KEEPS data for history
|
||||
function userDismissNotification(id) {
|
||||
const index = findNotificationIndex(id);
|
||||
// Dismiss a popup notification (e.g. clicked close, swipe, or overflow).
|
||||
// Removes from popup list but KEEPS data for history.
|
||||
function dismissPopup(id) {
|
||||
const index = findPopupIndex(id);
|
||||
if (index >= 0) {
|
||||
activeList.remove(index);
|
||||
popupModel.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
function dismissOldestActive() {
|
||||
if (activeList.count > 0) {
|
||||
const lastNotif = activeList.get(activeList.count - 1);
|
||||
dismissActiveNotification(lastNotif.id);
|
||||
function dismissOldestPopup() {
|
||||
if (popupModel.count > 0) {
|
||||
const lastNotif = popupModel.get(popupModel.count - 1);
|
||||
dismissPopup(lastNotif.id);
|
||||
}
|
||||
}
|
||||
|
||||
function dismissAllActive() {
|
||||
for (const id in activeNotifications) {
|
||||
activeNotifications[id].notification?.dismiss();
|
||||
activeNotifications[id].watcher?.destroy();
|
||||
function dismissAllPopups() {
|
||||
for (const id in popupState) {
|
||||
popupState[id].notification?.dismiss();
|
||||
popupState[id].watcher?.destroy();
|
||||
}
|
||||
activeList.clear();
|
||||
activeNotifications = {};
|
||||
popupModel.clear();
|
||||
popupState = {};
|
||||
quickshellIdToInternalId = {};
|
||||
}
|
||||
|
||||
function invokeActionAndSuppressClose(id, actionId) {
|
||||
const notifData = activeNotifications[id];
|
||||
const notifData = popupState[id];
|
||||
if (notifData && notifData.notification && notifData.onClosed) {
|
||||
try {
|
||||
notifData.notification.closed.disconnect(notifData.onClosed);
|
||||
@@ -920,7 +924,7 @@ Singleton {
|
||||
|
||||
function invokeAction(id, actionId) {
|
||||
let invoked = false;
|
||||
const notifData = activeNotifications[id];
|
||||
const notifData = popupState[id];
|
||||
|
||||
if (notifData && notifData.notification) {
|
||||
const actionsToUse = (notifData.notification.actions && notifData.notification.actions.length > 0) ? notifData.notification.actions : (notifData.cachedActions || []);
|
||||
@@ -957,9 +961,9 @@ Singleton {
|
||||
}
|
||||
} else if (!notifData) {
|
||||
Logger.w("NotificationService", "No active notification data for id=" + id + ", searching history for manual invoke");
|
||||
for (var i = 0; i < historyList.count; i++) {
|
||||
if (historyList.get(i).id === id) {
|
||||
const histEntry = historyList.get(i);
|
||||
for (var i = 0; i < historyModel.count; i++) {
|
||||
if (historyModel.get(i).id === id) {
|
||||
const histEntry = historyModel.get(i);
|
||||
if (histEntry.originalId) {
|
||||
invoked = manualInvoke(histEntry.originalId, actionId);
|
||||
}
|
||||
@@ -974,8 +978,8 @@ Singleton {
|
||||
}
|
||||
|
||||
// Clear actions after use
|
||||
updateModel(activeList, id, "actionsJson", "[]");
|
||||
updateModel(historyList, id, "actionsJson", "[]");
|
||||
updateModel(popupModel, id, "actionsJson", "[]");
|
||||
updateModel(historyModel, id, "actionsJson", "[]");
|
||||
saveHistory();
|
||||
|
||||
return true;
|
||||
@@ -1023,15 +1027,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function removeFromHistory(notificationId) {
|
||||
for (var i = 0; i < historyList.count; i++) {
|
||||
const notif = historyList.get(i);
|
||||
for (var i = 0; i < historyModel.count; i++) {
|
||||
const notif = historyModel.get(i);
|
||||
if (notif.id === notificationId) {
|
||||
// Only delete cached images that are in our cache directory
|
||||
const cachedPath = notif.cachedImage ? notif.cachedImage.replace(/^file:\/\//, "") : "";
|
||||
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
|
||||
Quickshell.execDetached(["rm", "-f", cachedPath]);
|
||||
}
|
||||
historyList.remove(i);
|
||||
historyModel.remove(i);
|
||||
saveHistory();
|
||||
return true;
|
||||
}
|
||||
@@ -1040,14 +1044,14 @@ Singleton {
|
||||
}
|
||||
|
||||
function removeOldestHistory() {
|
||||
if (historyList.count > 0) {
|
||||
const oldest = historyList.get(historyList.count - 1);
|
||||
if (historyModel.count > 0) {
|
||||
const oldest = historyModel.get(historyModel.count - 1);
|
||||
// Only delete cached images that are in our cache directory
|
||||
const cachedPath = oldest.cachedImage ? oldest.cachedImage.replace(/^file:\/\//, "") : "";
|
||||
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
|
||||
Quickshell.execDetached(["rm", "-f", cachedPath]);
|
||||
}
|
||||
historyList.remove(historyList.count - 1);
|
||||
historyModel.remove(historyModel.count - 1);
|
||||
saveHistory();
|
||||
return true;
|
||||
}
|
||||
@@ -1061,14 +1065,14 @@ Singleton {
|
||||
Logger.e("Notifications", "Failed to clear cache directory:", e);
|
||||
}
|
||||
|
||||
historyList.clear();
|
||||
historyModel.clear();
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
function getHistorySnapshot() {
|
||||
const items = [];
|
||||
for (var i = 0; i < historyList.count; i++) {
|
||||
const entry = historyList.get(i);
|
||||
for (var i = 0; i < historyModel.count; i++) {
|
||||
const entry = historyModel.get(i);
|
||||
items.push({
|
||||
"id": entry.id,
|
||||
"summary": entry.summary,
|
||||
|
||||
@@ -13,9 +13,11 @@ RowLayout {
|
||||
property bool hovering: false
|
||||
property color activeColor: Color.mPrimary
|
||||
property color activeOnColor: Color.mOnPrimary
|
||||
property int baseSize: Style.baseWidgetSize * 0.7
|
||||
property int baseSize: root.defaultSize
|
||||
property real labelSize: Style.fontSizeL
|
||||
|
||||
readonly property int defaultSize: Style.baseWidgetSize * 0.7
|
||||
|
||||
signal toggled(bool checked)
|
||||
signal entered
|
||||
signal exited
|
||||
@@ -41,7 +43,7 @@ RowLayout {
|
||||
Layout.margins: Style.borderS
|
||||
implicitWidth: Math.round(root.baseSize)
|
||||
implicitHeight: Math.round(root.baseSize)
|
||||
radius: Style.iRadiusXS
|
||||
radius: Style.iRadiusXS * (root.baseSize / root.defaultSize)
|
||||
color: root.checked ? root.activeColor : Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
Reference in New Issue
Block a user