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:
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user