1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

wallpaper: optimize caching and performance of wallpaper tab

This commit is contained in:
bbedward
2026-07-06 13:52:31 -04:00
parent cc0bec2682
commit 405ea708b3
6 changed files with 62 additions and 212 deletions
+1 -46
View File
@@ -7,62 +7,17 @@ import qs.Common
Singleton { Singleton {
id: root id: root
// Filenames present on disk, so CachingImage never points an Image at a
// missing cache file (Qt logs that as a "Cannot open" warning).
property var cachedFiles: ({})
Component.onCompleted: scan()
function scan() {
Proc.runCommand("imagecache_scan", ["ls", "-1", Paths.stringify(Paths.imagecache)], function (output, exitCode) {
const map = {};
if (exitCode === 0 && output) {
const lines = output.split("\n");
for (var i = 0; i < lines.length; i++) {
const name = lines[i].trim();
if (name)
map[name] = true;
}
}
root.cachedFiles = map;
});
}
function hasCachedFile(name) {
return name ? root.cachedFiles[name] === true : false;
}
function recordCachedFile(name) {
if (name)
root.cachedFiles[name] = true;
}
function forgetCachedFile(name) {
if (name && root.cachedFiles[name] !== undefined)
delete root.cachedFiles[name];
}
function clearImageCache() { function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]); Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
Paths.mkdir(Paths.imagecache); Paths.mkdir(Paths.imagecache);
root.cachedFiles = ({});
} }
function clearOldCache(ageInMinutes) { function clearOldCache(ageInMinutes) {
// Rescan on delete completion since we can't know which files matched. Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
Proc.runCommand("imagecache_prune", ["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"], function (output, exitCode) {
root.scan();
});
} }
function clearCacheForSize(size) { function clearCacheForSize(size) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]); Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
const suffix = `@${size}x${size}.png`;
const map = {};
for (const key in root.cachedFiles)
if (!key.endsWith(suffix))
map[key] = true;
root.cachedFiles = map;
} }
function getCacheSize(callback) { function getCacheSize(callback) {
+3
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtCore import QtCore
import QtQuick
import qs.Services import qs.Services
Singleton { Singleton {
@@ -19,6 +20,8 @@ Singleton {
readonly property url imagecache: `${cache}/imagecache` readonly property url imagecache: `${cache}/imagecache`
Component.onCompleted: mkdir(imagecache)
function stringify(path: url): string { function stringify(path: url): string {
return path.toString().replace(/%20/g, " "); return path.toString().replace(/%20/g, " ");
} }
@@ -447,6 +447,7 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
active: root.currentTabId === "wallpaper" active: root.currentTabId === "wallpaper"
visible: active visible: active
asynchronous: true
sourceComponent: Component { sourceComponent: Component {
WallpaperTab { WallpaperTab {
active: true active: true
@@ -458,6 +459,12 @@ DankPopout {
} }
} }
DankSpinner {
anchors.centerIn: parent
size: 40
visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading
}
Loader { Loader {
id: weatherLoader id: weatherLoader
anchors.fill: parent anchors.fill: parent
+38 -56
View File
@@ -1,7 +1,8 @@
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Effects import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Widgets import qs.Widgets
@@ -350,17 +351,6 @@ Item {
} }
} }
function collectWallpaperPaths() {
const paths = [];
for (var i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath");
if (filePath) {
paths.push(filePath.toString().replace(/^file:\/\//, ''));
}
}
return paths;
}
Connections { Connections {
target: wallpaperFolderModel target: wallpaperFolderModel
function onCountChanged() { function onCountChanged() {
@@ -369,7 +359,6 @@ Item {
setInitialSelection(); setInitialSelection();
} }
updateSelectedFileName(); updateSelectedFileName();
thumbnailPreloader.paths = collectWallpaperPaths();
} }
} }
function onStatusChanged() { function onStatusChanged() {
@@ -378,16 +367,10 @@ Item {
setInitialSelection(); setInitialSelection();
} }
updateSelectedFileName(); updateSelectedFileName();
thumbnailPreloader.paths = collectWallpaperPaths();
} }
} }
} }
WallpaperThumbnailPreloader {
id: thumbnailPreloader
cacheSize: 256
}
FolderListModel { FolderListModel {
id: wallpaperFolderModel id: wallpaperFolderModel
@@ -466,6 +449,7 @@ Item {
activeFocusOnTab: false activeFocusOnTab: false
focus: false focus: false
cacheBuffer: Math.max(0, height * 2) cacheBuffer: Math.max(0, height * 2)
reuseItems: true
model: root.totalPages model: root.totalPages
onCurrentIndexChanged: { onCurrentIndexChanged: {
@@ -517,18 +501,21 @@ Item {
} }
} }
model: { reuseItems: true
root.gridRevision; // re-evaluate when sort order changes in place model: ScriptModel {
const startIndex = pageIndex * root.itemsPerPage; values: {
const endIndex = Math.min(startIndex + root.itemsPerPage, wallpaperFolderModel.count); root.gridRevision; // re-evaluate when sort order changes in place
const items = []; const startIndex = pageGrid.pageIndex * root.itemsPerPage;
for (var i = startIndex; i < endIndex; i++) { const endIndex = Math.min(startIndex + root.itemsPerPage, wallpaperFolderModel.count);
const filePath = wallpaperFolderModel.get(i, "filePath"); const items = [];
if (filePath) { for (var i = startIndex; i < endIndex; i++) {
items.push(filePath.toString().replace(/^file:\/\//, '')); const filePath = wallpaperFolderModel.get(i, "filePath");
if (filePath) {
items.push(filePath.toString().replace(/^file:\/\//, ''));
}
} }
return items;
} }
return items;
} }
onCountChanged: { onCountChanged: {
@@ -553,7 +540,6 @@ Item {
anchors.margins: Theme.spacingXS anchors.margins: Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
clip: true
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -568,34 +554,24 @@ Item {
} }
} }
Rectangle { ClippingRectangle {
id: maskRect
width: thumbnailImage.width
height: thumbnailImage.height
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
CachingImage {
id: thumbnailImage
anchors.fill: parent anchors.fill: parent
imagePath: modelData || "" radius: wallpaperCard.radius
maxCacheSize: 256 color: "transparent"
opacity: status === Image.Ready ? 1 : 0
layer.enabled: true CachingImage {
layer.effect: MultiEffect { id: thumbnailImage
maskEnabled: true anchors.fill: parent
maskThresholdMin: 0.5 imagePath: modelData || ""
maskSpreadAtMin: 1.0 maxCacheSize: 256
maskSource: maskRect animate: false
} opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
}
} }
} }
} }
@@ -624,9 +600,15 @@ Item {
} }
} }
DankSpinner {
anchors.centerIn: parent
size: 40
visible: wallpaperFolderModel.status === FolderListModel.Loading && wallpaperFolderModel.count === 0
}
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
visible: wallpaperFolderModel.count === 0 visible: wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count === 0
text: I18n.tr("No wallpapers found\n\nClick the folder icon below to browse") text: I18n.tr("No wallpapers found\n\nClick the folder icon below to browse")
font.pixelSize: 14 font.pixelSize: 14
color: Theme.outline color: Theme.outline
+13 -16
View File
@@ -8,11 +8,14 @@ Item {
property int maxCacheSize: 512 property int maxCacheSize: 512
property int status: isAnimated ? animatedImg.status : staticImg.status property int status: isAnimated ? animatedImg.status : staticImg.status
property int fillMode: Image.PreserveAspectCrop property int fillMode: Image.PreserveAspectCrop
// AnimatedImage decodes full-size on the GUI thread and is never cached;
// disable for thumbnail grids
property bool animate: true
property bool _fromCache: false property bool _fromCache: false
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://") readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
readonly property bool isAnimated: { readonly property bool isAnimated: {
if (!imagePath) if (!animate || !imagePath)
return false; return false;
const lower = imagePath.toLowerCase(); const lower = imagePath.toLowerCase();
return lower.endsWith(".gif") || lower.endsWith(".webp"); return lower.endsWith(".gif") || lower.endsWith(".webp");
@@ -75,7 +78,6 @@ Item {
if (!root._fromCache) if (!root._fromCache)
return; return;
root._fromCache = false; root._fromCache = false;
CacheUtils.forgetCachedFile(root.cacheFileName);
source = root.encodedImagePath; source = root.encodedImagePath;
return; return;
case Image.Ready: case Image.Ready:
@@ -83,19 +85,16 @@ Item {
return; return;
if (!visible || width <= 0 || height <= 0 || !Window.window?.visible) if (!visible || width <= 0 || height <= 0 || !Window.window?.visible)
return; return;
Paths.mkdir(Paths.imagecache);
const grabPath = root.cachePath; const grabPath = root.cachePath;
const grabName = root.cacheFileName;
grabToImage(res => { grabToImage(res => {
if (res.saveToFile(grabPath)) res.saveToFile(grabPath);
CacheUtils.recordCachedFile(grabName);
}); });
return; return;
} }
} }
} }
onImagePathChanged: { function resolveSource() {
if (!imagePath) { if (!imagePath) {
_fromCache = false; _fromCache = false;
staticImg.source = ""; staticImg.source = "";
@@ -113,14 +112,12 @@ Item {
staticImg.source = encodedImagePath; staticImg.source = encodedImagePath;
return; return;
} }
Paths.mkdir(Paths.imagecache); // Cache-first; a miss errors and falls back to encodedImagePath
// Read cache only when present; else load source and cache it on Ready _fromCache = true;
if (CacheUtils.hasCachedFile(cacheFileName)) { staticImg.source = cachePath;
_fromCache = true;
staticImg.source = cachePath;
} else {
_fromCache = false;
staticImg.source = encodedImagePath;
}
} }
onImagePathChanged: resolveSource()
// During creation onImagePathChanged fires before sibling properties (maxCacheSize) initialize
onCachePathChanged: resolveSource()
} }
@@ -1,94 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
// Preload the CachingImage disk cache for a folder of wallpapers via ffmpegthumbnailer
// so the switcher grid renders instantly. No-op (graceful fallback) if the tool is absent.
Item {
id: root
visible: false
property var paths: []
property int cacheSize: 256
property bool autoStart: true
property int maxConcurrent: 3
property int _active: 0
property var _queue: []
property int _toolState: -1 // -1 unknown, 0 unavailable, 1 available
onPathsChanged: if (autoStart)
preload()
// Must match djb2Hash + cachePath in Widgets/CachingImage.qml.
function _hash(str) {
if (!str)
return "";
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & 0x7FFFFFFF;
}
return hash.toString(16).padStart(8, '0');
}
function _cachePathFor(path) {
const hash = _hash(path);
if (!hash)
return "";
return `${Paths.stringify(Paths.imagecache)}/${hash}@${cacheSize}x${cacheSize}.png`;
}
function _isAnimated(path) {
const lower = path.toLowerCase();
return lower.endsWith(".gif") || lower.endsWith(".webp");
}
function preload() {
if (!paths || paths.length === 0 || _toolState === 0)
return;
if (_toolState === -1) {
Proc.runCommand("wallpaperThumbToolCheck", ["sh", "-c", "command -v ffmpegthumbnailer"], function (out, code) {
root._toolState = code === 0 ? 1 : 0;
if (root._toolState === 1)
root._start();
});
return;
}
_start();
}
function _start() {
Paths.mkdir(Paths.imagecache);
const q = [];
for (let i = 0; i < paths.length; i++) {
const p = paths[i];
if (!p || p.startsWith("#") || _isAnimated(p))
continue;
q.push(p);
}
_queue = q;
for (let i = 0; i < maxConcurrent; i++)
_pump();
}
function _pump() {
if (_queue.length === 0)
return;
const path = _queue.shift();
const cachePath = _cachePathFor(path);
if (!cachePath) {
_pump();
return;
}
_active++;
// One process per file: skip if already cached, otherwise generate.
const script = "test -f \"$1\" || ffmpegthumbnailer -i \"$2\" -o \"$1\" -s " + cacheSize;
Proc.runCommand(null, ["sh", "-c", script, "thumb", cachePath, path], function (out, code) {
root._active--;
root._pump();
}, 0, 20000);
}
}