diff --git a/quickshell/Modules/DesktopWidgetLayer.qml b/quickshell/Modules/DesktopWidgetLayer.qml index da0eca42..384025f6 100644 --- a/quickshell/Modules/DesktopWidgetLayer.qml +++ b/quickshell/Modules/DesktopWidgetLayer.qml @@ -103,7 +103,7 @@ Variants { } pluginService: (liveInstanceData.widgetType !== "desktopClock" && liveInstanceData.widgetType !== "systemMonitor") ? PluginService : null screen: screenDelegate.screen - visible: shouldBeVisible + widgetEnabled: shouldBeVisible } } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 1039b6fa..251a7890 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -362,6 +362,7 @@ PanelWindow { id: iconContainer readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" + readonly property bool needsImagePersist: hasNotificationImage && notificationData.image.startsWith("image://qsimage/") && !notificationData.persistedImagePath width: 63 height: 63 @@ -391,6 +392,22 @@ PanelWindow { const appName = notificationData?.appName || "?"; return appName.charAt(0).toUpperCase(); } + + onImageStatusChanged: { + if (imageStatus === Image.Ready && needsImagePersist) { + const cachePath = NotificationService.getImageCachePath(notificationData); + saveImageToFile(cachePath); + } + } + + onImageSaved: filePath => { + if (!notificationData) + return; + notificationData.persistedImagePath = filePath; + const wrapperId = notificationData.notification?.id?.toString() || ""; + if (wrapperId) + NotificationService.updateHistoryImage(wrapperId, filePath); + } } Rectangle { diff --git a/quickshell/Modules/Plugins/DesktopPluginWrapper.qml b/quickshell/Modules/Plugins/DesktopPluginWrapper.qml index 4854530d..f79ea423 100644 --- a/quickshell/Modules/Plugins/DesktopPluginWrapper.qml +++ b/quickshell/Modules/Plugins/DesktopPluginWrapper.qml @@ -17,6 +17,7 @@ Item { property var pluginService: null property string instanceId: "" property var instanceData: null + property bool widgetEnabled: true readonly property bool isBuiltin: pluginId === "desktopClock" || pluginId === "systemMonitor" readonly property var activeComponent: isBuiltin ? builtinComponent : PluginService.pluginDesktopComponents[pluginId] ?? null @@ -202,7 +203,7 @@ Item { PanelWindow { id: widgetWindow screen: root.screen - visible: root.visible && root.activeComponent !== null + visible: root.widgetEnabled && root.activeComponent !== null color: "transparent" WlrLayershell.namespace: "quickshell:desktop-widget:" + root.pluginId + (root.instanceId ? ":" + root.instanceId : "") diff --git a/quickshell/Services/NotificationService.qml b/quickshell/Services/NotificationService.qml index 5c794a6e..e7d82141 100644 --- a/quickshell/Services/NotificationService.qml +++ b/quickshell/Services/NotificationService.qml @@ -17,6 +17,7 @@ Singleton { property var historyList: [] readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json" + readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images" property bool historyLoaded: false property list notificationQueue: [] @@ -46,6 +47,7 @@ Singleton { Component.onCompleted: { _recomputeGroups(); Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]); + Quickshell.execDetached(["mkdir", "-p", imageCacheDir]); } FileView { @@ -72,10 +74,46 @@ Singleton { onTriggered: root.performSaveHistory() } + function getImageCachePath(wrapper) { + const ts = wrapper.time ? wrapper.time.getTime() : Date.now(); + const id = wrapper.notification?.id?.toString() || "0"; + return imageCacheDir + "/notif_" + ts + "_" + id + ".png"; + } + + function updateHistoryImage(wrapperId, imagePath) { + const idx = historyList.findIndex(n => n.id === wrapperId); + if (idx < 0) + return; + const item = historyList[idx]; + const updated = { + id: item.id, + summary: item.summary, + body: item.body, + htmlBody: item.htmlBody, + appName: item.appName, + appIcon: item.appIcon, + image: "file://" + imagePath, + urgency: item.urgency, + timestamp: item.timestamp, + desktopEntry: item.desktopEntry + }; + const newList = historyList.slice(); + newList[idx] = updated; + historyList = newList; + saveHistory(); + } + function addToHistory(wrapper) { if (!wrapper) return; const urg = typeof wrapper.urgency === "number" ? wrapper.urgency : 1; + const imageUrl = wrapper.image || ""; + let persistableImage = ""; + if (wrapper.persistedImagePath) { + persistableImage = "file://" + wrapper.persistedImagePath; + } else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) { + persistableImage = imageUrl; + } const data = { id: wrapper.notification?.id?.toString() || Date.now().toString(), summary: wrapper.summary || "", @@ -83,7 +121,7 @@ Singleton { htmlBody: wrapper.htmlBody || wrapper.body || "", appName: wrapper.appName || "", appIcon: wrapper.appIcon || "", - image: wrapper.cleanImage || "", + image: persistableImage, urgency: urg, timestamp: wrapper.time.getTime(), desktopEntry: wrapper.desktopEntry || "" @@ -148,9 +186,19 @@ Singleton { } } + function _deleteCachedImage(imagePath) { + if (!imagePath || !imagePath.startsWith("file://")) + return; + const filePath = imagePath.replace("file://", ""); + if (filePath.startsWith(imageCacheDir)) { + Quickshell.execDetached(["rm", "-f", filePath]); + } + } + function removeFromHistory(notificationId) { const idx = historyList.findIndex(n => n.id === notificationId); if (idx >= 0) { + _deleteCachedImage(historyList[idx].image); historyList = historyList.filter((_, i) => i !== idx); saveHistory(); return true; @@ -159,6 +207,9 @@ Singleton { } function clearHistory() { + for (const item of historyList) { + _deleteCachedImage(item.image); + } historyList = []; saveHistory(); } @@ -268,15 +319,22 @@ Singleton { const now = Date.now(); const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000; + const toRemove = historyList.filter(item => (now - item.timestamp) > maxAgeMs); const pruned = historyList.filter(item => (now - item.timestamp) <= maxAgeMs); if (pruned.length !== historyList.length) { + for (const item of toRemove) { + _deleteCachedImage(item.image); + } historyList = pruned; saveHistory(); } } function deleteHistory() { + for (const item of historyList) { + _deleteCachedImage(item.image); + } historyList = []; historyAdapter.notifications = []; historyFileView.writeAdapter(); @@ -461,6 +519,7 @@ Singleton { property bool removedByLimit: false property bool isPersistent: true property int seq: 0 + property string persistedImagePath: "" onPopupChanged: { if (!popup) { diff --git a/quickshell/Widgets/DankCircularImage.qml b/quickshell/Widgets/DankCircularImage.qml index e54289e4..81d2c786 100644 --- a/quickshell/Widgets/DankCircularImage.qml +++ b/quickshell/Widgets/DankCircularImage.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Effects -import Quickshell import qs.Common import qs.Widgets @@ -13,6 +12,19 @@ Rectangle { property bool hasImage: imageSource !== "" property alias imageStatus: internalImage.status + signal imageSaved(string filePath) + + function saveImageToFile(filePath) { + if (internalImage.status !== Image.Ready) + return false; + internalImage.grabToImage(function (result) { + if (result && result.saveToFile(filePath)) { + root.imageSaved(filePath); + } + }); + return true; + } + radius: width / 2 color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) border.color: "transparent" @@ -67,7 +79,6 @@ Rectangle { visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== "" } - StyledText { anchors.centerIn: parent visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""