From 89e359015e1445edd4c1fb33a9d5992e2008982c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 17:48:39 -0400 Subject: [PATCH] Wallpapers: added thumbnail caching. Had to drop the rounded borders for it to work. NImageCached relies on the image being visible to work properly. MultiEffect relies on the image being invisible to apply effects. That's why we don't have rounded corners here, as we don't want to bring back qt5compat. --- Commons/Settings.qml | 2 + .../Tabs/WallpaperSelectorTab.qml | 29 +++++---- Widgets/NImageCached.qml | 59 +++++++++++++++++++ 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 Widgets/NImageCached.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index eed8bbe..8e00047 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -17,6 +17,7 @@ Singleton { "HOME") + "/.config") + "/" + shellName + "/" property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( "HOME") + "/.cache") + "/" + shellName + "/" + property string cacheDirImages: cacheDir + "images/" property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") @@ -37,6 +38,7 @@ Singleton { // ensure settings dir exists Quickshell.execDetached(["mkdir", "-p", configDir]) Quickshell.execDetached(["mkdir", "-p", cacheDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index e0412e0..e0eb3bb 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -135,27 +135,32 @@ Item { delegate: Rectangle { id: wallpaperItem + property string wallpaperPath: modelData property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) - radius: Style.radiusMedium * scaling - color: isSelected ? Color.mPrimary : Color.mSurface - border.color: isSelected ? Color.mSecondary : Color.mOutline - border.width: Math.max(1, Style.borderThin * scaling) - clip: true + color: Color.transparent - NImageRounded { - anchors.fill: parent - anchors.margins: Style.marginTiny * scaling + // NImageCached relies on the image being visible to work properly. + // MultiEffect relies on the image being invisible to apply effects. + // That's why we don't have rounded corners here, as we don't want to bring back qt5compat. + NImageCached { + id: img imagePath: wallpaperPath - fallbackIcon: "image" - - imageRadius: Style.radiusMedium * scaling + anchors.fill: parent } - // Selection indicator + // Borders on top + Rectangle { + anchors.fill: parent + color: Color.transparent + border.color: isSelected ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderThick * scaling) + } + + // Selection tick-mark Rectangle { anchors.top: parent.top anchors.right: parent.right diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml new file mode 100644 index 0000000..3980370 --- /dev/null +++ b/Widgets/NImageCached.qml @@ -0,0 +1,59 @@ +pragma ComponentBehavior + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Image { + id: root + + property string imagePath: "" + property string imageHash: "" + property int maxCacheDimension: 512 + readonly property string cachePath: imageHash ? `${Settings.cacheDirImages}${imageHash}@${maxCacheDimension}x${maxCacheDimension}.png` : "" + + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize.width: maxCacheDimension + sourceSize.height: maxCacheDimension + smooth: true + onImagePathChanged: { + if (imagePath) { + hashProcess.command = ["sha256sum", imagePath] + hashProcess.running = true + } else { + source = "" + imageHash = "" + } + } + onCachePathChanged: { + if (imageHash && cachePath) { + // Try to load the cached version, failure will be detected below in onStatusChanged + source = cachePath + //Logger.Log(imagePath, cachePath) + } + } + onStatusChanged: { + if (source == cachePath && status === Image.Error) { + // Cached image was not available, show the original + source = imagePath + } else if (source == imagePath && status === Image.Ready && imageHash && cachePath) { + // Original image is shown and fully loaded, time to cache it + const grabPath = cachePath + if (visible && width > 0 && height > 0 && Window.window && Window.window.visible) + grabToImage(res => { + return res.saveToFile(grabPath) + }) + } + } + + Process { + id: hashProcess + stdout: StdioCollector { + onStreamFinished: { + root.imageHash = text.split(" ")[0] + } + } + } +}