mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
785 lines
28 KiB
QML
785 lines
28 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import "../../Helpers/sha256.js" as Checksum
|
|
import qs.Commons
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// -------------------------------------------------
|
|
// Public Properties
|
|
// -------------------------------------------------
|
|
property bool imageMagickAvailable: false
|
|
property bool initialized: false
|
|
|
|
// Cache directories
|
|
readonly property string baseDir: Settings.cacheDir + "images/"
|
|
readonly property string wpThumbDir: baseDir + "wallpapers/thumbnails/"
|
|
readonly property string wpLargeDir: baseDir + "wallpapers/large/"
|
|
readonly property string notificationsDir: baseDir + "notifications/"
|
|
readonly property string contributorsDir: baseDir + "contributors/"
|
|
|
|
// Supported image formats - extended list when ImageMagick is available
|
|
readonly property var basicImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"]
|
|
readonly property var extendedImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.webp", "*.avif", "*.heic", "*.heif", "*.tiff", "*.tif", "*.pnm", "*.pgm", "*.ppm", "*.pbm", "*.svg", "*.svgz", "*.ico", "*.icns", "*.jxl", "*.jp2", "*.j2k", "*.exr", "*.hdr", "*.dds", "*.tga"]
|
|
readonly property var imageFilters: imageMagickAvailable ? extendedImageFilters : basicImageFilters
|
|
|
|
// Check if a file format needs conversion (not natively supported by Qt)
|
|
function needsConversion(filePath) {
|
|
const ext = "*." + filePath.toLowerCase().split('.').pop();
|
|
return !basicImageFilters.includes(ext);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Internal State
|
|
// -------------------------------------------------
|
|
property var pendingRequests: ({})
|
|
property var fallbackQueue: []
|
|
property bool fallbackProcessing: false
|
|
|
|
// Process queues to prevent "too many open files" errors
|
|
property var utilityProcessQueue: []
|
|
property int runningUtilityProcesses: 0
|
|
readonly property int maxConcurrentUtilityProcesses: 16
|
|
|
|
// Separate queue for heavy ImageMagick processing (lower concurrency)
|
|
property var imageMagickQueue: []
|
|
property int runningImageMagickProcesses: 0
|
|
readonly property int maxConcurrentImageMagickProcesses: 4
|
|
|
|
// -------------------------------------------------
|
|
// Signals
|
|
// -------------------------------------------------
|
|
signal cacheHit(string cacheKey, string cachedPath)
|
|
signal cacheMiss(string cacheKey)
|
|
signal processingComplete(string cacheKey, string cachedPath)
|
|
signal processingFailed(string cacheKey, string error)
|
|
|
|
// -------------------------------------------------
|
|
// Initialization
|
|
// -------------------------------------------------
|
|
function init() {
|
|
Logger.i("ImageCache", "Service started");
|
|
createDirectories();
|
|
cleanupOldCache();
|
|
checkMagickProcess.running = true;
|
|
}
|
|
|
|
function createDirectories() {
|
|
Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", wpLargeDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
|
|
}
|
|
|
|
function cleanupOldCache() {
|
|
const dirs = [wpThumbDir, wpLargeDir, notificationsDir, contributorsDir];
|
|
dirs.forEach(function (dir) {
|
|
Quickshell.execDetached(["find", dir, "-type", "f", "-mtime", "+15", "-delete"]);
|
|
});
|
|
Logger.d("ImageCache", "Cleanup triggered for files older than 15 days");
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Public API: Get Thumbnail (384x384)
|
|
// -------------------------------------------------
|
|
function getThumbnail(sourcePath, callback) {
|
|
if (!sourcePath || sourcePath === "") {
|
|
callback("", false);
|
|
return;
|
|
}
|
|
|
|
getMtime(sourcePath, function (mtime) {
|
|
const cacheKey = generateThumbnailKey(sourcePath, mtime);
|
|
const cachedPath = wpThumbDir + cacheKey + ".png";
|
|
|
|
processRequest(cacheKey, cachedPath, sourcePath, callback, function () {
|
|
if (imageMagickAvailable) {
|
|
startThumbnailProcessing(sourcePath, cachedPath, cacheKey);
|
|
} else {
|
|
queueFallbackProcessing(sourcePath, cachedPath, cacheKey, 384);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Public API: Get Large Image (scaled to specified dimensions)
|
|
// -------------------------------------------------
|
|
function getLarge(sourcePath, width, height, callback) {
|
|
if (!sourcePath || sourcePath === "") {
|
|
callback("", false);
|
|
return;
|
|
}
|
|
|
|
if (Settings.data.wallpaper.useOriginalImages && !needsConversion(sourcePath)) {
|
|
callback(sourcePath, false);
|
|
return;
|
|
}
|
|
|
|
if (!imageMagickAvailable) {
|
|
Logger.d("ImageCache", "ImageMagick not available, using original:", sourcePath);
|
|
callback(sourcePath, false);
|
|
return;
|
|
}
|
|
|
|
// Fast dimension check - skip processing if image fits screen AND format is Qt-native
|
|
getImageDimensions(sourcePath, function (imgWidth, imgHeight) {
|
|
const fitsScreen = imgWidth > 0 && imgHeight > 0 && imgWidth <= width && imgHeight <= height;
|
|
|
|
if (fitsScreen) {
|
|
// Only skip if format is natively supported by Qt
|
|
if (!needsConversion(sourcePath)) {
|
|
Logger.d("ImageCache", `Image ${imgWidth}x${imgHeight} fits screen ${width}x${height}, using original`);
|
|
callback(sourcePath, false);
|
|
return;
|
|
}
|
|
Logger.d("ImageCache", `Image needs conversion despite fitting screen`);
|
|
}
|
|
|
|
// Use actual image dimensions if it fits (convert without upscaling), otherwise use screen dimensions
|
|
const targetWidth = fitsScreen ? imgWidth : width;
|
|
const targetHeight = fitsScreen ? imgHeight : height;
|
|
|
|
getMtime(sourcePath, function (mtime) {
|
|
const cacheKey = generateLargeKey(sourcePath, width, height, mtime);
|
|
const cachedPath = wpLargeDir + cacheKey + ".png";
|
|
|
|
processRequest(cacheKey, cachedPath, sourcePath, callback, function () {
|
|
startLargeProcessing(sourcePath, cachedPath, targetWidth, targetHeight, cacheKey);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Public API: Get Notification Icon (64x64)
|
|
// -------------------------------------------------
|
|
function getNotificationIcon(imageUri, appName, summary, callback) {
|
|
if (!imageUri || imageUri === "") {
|
|
callback("", false);
|
|
return;
|
|
}
|
|
|
|
// Resolve bare file path for temp check
|
|
const filePath = imageUri.startsWith("file://") ? imageUri.substring(7) : imageUri;
|
|
|
|
// File paths in persistent locations are used directly, not cached
|
|
if ((imageUri.startsWith("/") || imageUri.startsWith("file://")) && !isTemporaryPath(filePath)) {
|
|
callback(imageUri, false);
|
|
return;
|
|
}
|
|
|
|
const cacheKey = generateNotificationKey(imageUri, appName, summary);
|
|
const cachedPath = notificationsDir + cacheKey + ".png";
|
|
|
|
// Temporary file paths are copied to cache before the source is cleaned up
|
|
if (imageUri.startsWith("/") || imageUri.startsWith("file://")) {
|
|
processRequest(cacheKey, cachedPath, imageUri, callback, function () {
|
|
copyTempFileToCache(filePath, cachedPath, cacheKey);
|
|
});
|
|
return;
|
|
}
|
|
|
|
processRequest(cacheKey, cachedPath, imageUri, callback, function () {
|
|
// Notifications always use Qt fallback (image:// URIs can't be read by ImageMagick)
|
|
queueFallbackProcessing(imageUri, cachedPath, cacheKey, 64);
|
|
});
|
|
}
|
|
|
|
// Check if a path is in a temporary directory that may be cleaned up
|
|
function isTemporaryPath(path) {
|
|
return path.startsWith("/tmp/");
|
|
}
|
|
|
|
// Copy a temporary file to the cache directory
|
|
function copyTempFileToCache(sourcePath, destPath, cacheKey) {
|
|
const srcEsc = sourcePath.replace(/'/g, "'\\''");
|
|
const dstEsc = destPath.replace(/'/g, "'\\''");
|
|
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["cp", "--", "${srcEsc}", "${dstEsc}"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
queueUtilityProcess({
|
|
name: "CopyTempFile_" + cacheKey,
|
|
processString: processString,
|
|
onComplete: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
Logger.d("ImageCache", "Temp file cached:", destPath);
|
|
notifyCallbacks(cacheKey, destPath, true);
|
|
} else {
|
|
Logger.w("ImageCache", "Failed to cache temp file:", sourcePath);
|
|
notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
},
|
|
onError: function () {
|
|
Logger.e("ImageCache", "Error caching temp file:", sourcePath);
|
|
notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Public API: Get Circular Avatar (256x256)
|
|
// -------------------------------------------------
|
|
function getCircularAvatar(url, username, callback) {
|
|
if (!url || !username) {
|
|
callback("", false);
|
|
return;
|
|
}
|
|
|
|
const cacheKey = username;
|
|
const cachedPath = contributorsDir + username + "_circular.png";
|
|
|
|
processRequest(cacheKey, cachedPath, url, callback, function () {
|
|
if (imageMagickAvailable) {
|
|
downloadAndProcessAvatar(url, username, cachedPath, cacheKey);
|
|
} else {
|
|
// No fallback for circular avatars without ImageMagick
|
|
Logger.w("ImageCache", "Circular avatars require ImageMagick");
|
|
notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Cache Key Generation
|
|
// -------------------------------------------------
|
|
function generateThumbnailKey(sourcePath, mtime) {
|
|
const keyString = sourcePath + "@384x384@" + (mtime || "unknown");
|
|
return Checksum.sha256(keyString);
|
|
}
|
|
|
|
function generateLargeKey(sourcePath, width, height, mtime) {
|
|
const keyString = sourcePath + "@" + width + "x" + height + "@" + (mtime || "unknown");
|
|
return Checksum.sha256(keyString);
|
|
}
|
|
|
|
function generateNotificationKey(imageUri, appName, summary) {
|
|
if (imageUri.startsWith("image://qsimage/")) {
|
|
return Checksum.sha256(appName + "|" + summary);
|
|
}
|
|
return Checksum.sha256(imageUri);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Request Processing (with coalescing)
|
|
// -------------------------------------------------
|
|
function processRequest(cacheKey, cachedPath, sourcePath, callback, processFn) {
|
|
// Check if already processing this request
|
|
if (pendingRequests[cacheKey]) {
|
|
pendingRequests[cacheKey].callbacks.push(callback);
|
|
Logger.d("ImageCache", "Coalescing request for:", cacheKey);
|
|
return;
|
|
}
|
|
|
|
// Check cache first
|
|
checkFileExists(cachedPath, function (exists) {
|
|
if (exists) {
|
|
Logger.d("ImageCache", "Cache hit:", cachedPath);
|
|
callback(cachedPath, true);
|
|
cacheHit(cacheKey, cachedPath);
|
|
return;
|
|
}
|
|
|
|
// Re-check pendingRequests (race condition fix)
|
|
if (pendingRequests[cacheKey]) {
|
|
pendingRequests[cacheKey].callbacks.push(callback);
|
|
return;
|
|
}
|
|
|
|
// Start new processing
|
|
Logger.d("ImageCache", "Cache miss, processing:", sourcePath);
|
|
cacheMiss(cacheKey);
|
|
pendingRequests[cacheKey] = {
|
|
callbacks: [callback],
|
|
sourcePath: sourcePath
|
|
};
|
|
|
|
processFn();
|
|
});
|
|
}
|
|
|
|
function notifyCallbacks(cacheKey, path, success) {
|
|
const request = pendingRequests[cacheKey];
|
|
if (request) {
|
|
request.callbacks.forEach(function (cb) {
|
|
cb(path, success);
|
|
});
|
|
delete pendingRequests[cacheKey];
|
|
}
|
|
|
|
if (success) {
|
|
processingComplete(cacheKey, path);
|
|
} else {
|
|
processingFailed(cacheKey, "Processing failed");
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// ImageMagick Processing: Thumbnail
|
|
// -------------------------------------------------
|
|
function startThumbnailProcessing(sourcePath, outputPath, cacheKey) {
|
|
const srcEsc = sourcePath.replace(/'/g, "'\\''");
|
|
const dstEsc = outputPath.replace(/'/g, "'\\''");
|
|
|
|
// Use Lanczos filter for high-quality downscaling, subtle unsharp mask, and PNG for lossless output
|
|
const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '384x384^' -gravity center -extent 384x384 -unsharp 0x0.5 '${dstEsc}'`;
|
|
|
|
runProcess(command, cacheKey, outputPath, sourcePath);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// ImageMagick Processing: Large
|
|
// -------------------------------------------------
|
|
function startLargeProcessing(sourcePath, outputPath, width, height, cacheKey) {
|
|
const srcEsc = sourcePath.replace(/'/g, "'\\''");
|
|
const dstEsc = outputPath.replace(/'/g, "'\\''");
|
|
|
|
// Use Lanczos filter for high-quality downscaling and PNG for lossless output
|
|
const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '${width}x${height}^' '${dstEsc}'`;
|
|
|
|
runProcess(command, cacheKey, outputPath, sourcePath);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// ImageMagick Processing: Circular Avatar
|
|
// -------------------------------------------------
|
|
function downloadAndProcessAvatar(url, username, outputPath, cacheKey) {
|
|
const tempPath = contributorsDir + username + "_temp.png";
|
|
const tempEsc = tempPath.replace(/'/g, "'\\''");
|
|
const urlEsc = url.replace(/'/g, "'\\''");
|
|
|
|
// Download first (uses utility queue since curl/wget are lightweight)
|
|
const downloadCmd = `curl -L -s -o '${tempEsc}' '${urlEsc}' || wget -q -O '${tempEsc}' '${urlEsc}'`;
|
|
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["sh", "-c", "${downloadCmd.replace(/"/g, '\\"')}"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
queueUtilityProcess({
|
|
name: "DownloadProcess_" + cacheKey,
|
|
processString: processString,
|
|
onComplete: function (exitCode) {
|
|
if (exitCode !== 0) {
|
|
Logger.e("ImageCache", "Failed to download avatar for", username);
|
|
notifyCallbacks(cacheKey, "", false);
|
|
return;
|
|
}
|
|
// Now process with ImageMagick
|
|
processCircularAvatar(tempPath, outputPath, cacheKey);
|
|
},
|
|
onError: function () {
|
|
notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function processCircularAvatar(inputPath, outputPath, cacheKey) {
|
|
const srcEsc = inputPath.replace(/'/g, "'\\''");
|
|
const dstEsc = outputPath.replace(/'/g, "'\\''");
|
|
|
|
// ImageMagick command for circular crop with alpha
|
|
const command = `magick '${srcEsc}' -resize 256x256^ -gravity center -extent 256x256 -alpha set \\( +clone -channel A -evaluate set 0 +channel -fill white -draw 'circle 128,128 128,0' \\) -compose DstIn -composite '${dstEsc}'`;
|
|
|
|
queueImageMagickProcess({
|
|
command: command,
|
|
cacheKey: cacheKey,
|
|
onComplete: function (exitCode) {
|
|
// Clean up temp file
|
|
Quickshell.execDetached(["rm", "-f", inputPath]);
|
|
|
|
if (exitCode !== 0) {
|
|
Logger.e("ImageCache", "Failed to create circular avatar");
|
|
notifyCallbacks(cacheKey, "", false);
|
|
} else {
|
|
Logger.d("ImageCache", "Circular avatar created:", outputPath);
|
|
notifyCallbacks(cacheKey, outputPath, true);
|
|
}
|
|
},
|
|
onError: function () {
|
|
Quickshell.execDetached(["rm", "-f", inputPath]);
|
|
notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Generic Process Runner (with queue for ImageMagick)
|
|
// -------------------------------------------------
|
|
|
|
// Queue an ImageMagick process and run it when a slot is available
|
|
function queueImageMagickProcess(request) {
|
|
imageMagickQueue.push(request);
|
|
processImageMagickQueue();
|
|
}
|
|
|
|
// Process queued ImageMagick requests up to the concurrency limit
|
|
function processImageMagickQueue() {
|
|
while (runningImageMagickProcesses < maxConcurrentImageMagickProcesses && imageMagickQueue.length > 0) {
|
|
const request = imageMagickQueue.shift();
|
|
runImageMagickProcess(request);
|
|
}
|
|
}
|
|
|
|
// Actually run an ImageMagick process
|
|
function runImageMagickProcess(request) {
|
|
runningImageMagickProcesses++;
|
|
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["sh", "-c", ""]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
try {
|
|
const processObj = Qt.createQmlObject(processString, root, "ImageProcess_" + request.cacheKey);
|
|
processObj.command = ["sh", "-c", request.command];
|
|
|
|
processObj.exited.connect(function (exitCode) {
|
|
processObj.destroy();
|
|
runningImageMagickProcesses--;
|
|
request.onComplete(exitCode, processObj);
|
|
processImageMagickQueue();
|
|
});
|
|
|
|
processObj.running = true;
|
|
} catch (e) {
|
|
Logger.e("ImageCache", "Failed to create process:", e);
|
|
runningImageMagickProcesses--;
|
|
request.onError(e);
|
|
processImageMagickQueue();
|
|
}
|
|
}
|
|
|
|
function runProcess(command, cacheKey, outputPath, sourcePath) {
|
|
queueImageMagickProcess({
|
|
command: command,
|
|
cacheKey: cacheKey,
|
|
onComplete: function (exitCode, proc) {
|
|
if (exitCode !== 0) {
|
|
const stderrText = proc.stderr.text || "";
|
|
Logger.e("ImageCache", "Processing failed:", stderrText);
|
|
notifyCallbacks(cacheKey, sourcePath, false);
|
|
} else {
|
|
Logger.d("ImageCache", "Processing complete:", outputPath);
|
|
notifyCallbacks(cacheKey, outputPath, true);
|
|
}
|
|
},
|
|
onError: function () {
|
|
notifyCallbacks(cacheKey, sourcePath, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Qt Fallback Renderer
|
|
// -------------------------------------------------
|
|
PanelWindow {
|
|
id: fallbackRenderer
|
|
implicitWidth: 0
|
|
implicitHeight: 0
|
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.namespace: "noctalia-image-cache-renderer"
|
|
color: "transparent"
|
|
mask: Region {}
|
|
|
|
Image {
|
|
id: fallbackImage
|
|
property string cacheKey: ""
|
|
property string destPath: ""
|
|
property int targetSize: 256
|
|
|
|
width: targetSize
|
|
height: targetSize
|
|
visible: true
|
|
cache: false
|
|
asynchronous: true
|
|
fillMode: Image.PreserveAspectCrop
|
|
mipmap: true
|
|
antialiasing: true
|
|
|
|
onStatusChanged: {
|
|
if (!cacheKey)
|
|
return;
|
|
|
|
if (status === Image.Ready) {
|
|
grabToImage(function (result) {
|
|
if (result.saveToFile(destPath)) {
|
|
Logger.d("ImageCache", "Fallback cache created:", destPath);
|
|
root.notifyCallbacks(cacheKey, destPath, true);
|
|
} else {
|
|
Logger.e("ImageCache", "Failed to save fallback cache");
|
|
root.notifyCallbacks(cacheKey, "", false);
|
|
}
|
|
processNextFallback();
|
|
});
|
|
} else if (status === Image.Error) {
|
|
Logger.e("ImageCache", "Fallback image load failed");
|
|
root.notifyCallbacks(cacheKey, "", false);
|
|
processNextFallback();
|
|
}
|
|
}
|
|
|
|
function processNextFallback() {
|
|
cacheKey = "";
|
|
destPath = "";
|
|
source = "";
|
|
|
|
if (fallbackQueue.length > 0) {
|
|
const next = fallbackQueue.shift();
|
|
cacheKey = next.cacheKey;
|
|
destPath = next.destPath;
|
|
targetSize = next.size;
|
|
source = next.sourcePath;
|
|
} else {
|
|
fallbackProcessing = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function queueFallbackProcessing(sourcePath, destPath, cacheKey, size) {
|
|
fallbackQueue.push({
|
|
sourcePath: sourcePath,
|
|
destPath: destPath,
|
|
cacheKey: cacheKey,
|
|
size: size
|
|
});
|
|
|
|
if (!fallbackProcessing) {
|
|
fallbackProcessing = true;
|
|
const item = fallbackQueue.shift();
|
|
fallbackImage.cacheKey = item.cacheKey;
|
|
fallbackImage.destPath = item.destPath;
|
|
fallbackImage.targetSize = item.size;
|
|
fallbackImage.source = item.sourcePath;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Utility Functions (with process queue to prevent fd exhaustion)
|
|
// -------------------------------------------------
|
|
|
|
// Queue a utility process and run it when a slot is available
|
|
function queueUtilityProcess(request) {
|
|
utilityProcessQueue.push(request);
|
|
processUtilityQueue();
|
|
}
|
|
|
|
// Process queued utility requests up to the concurrency limit
|
|
function processUtilityQueue() {
|
|
while (runningUtilityProcesses < maxConcurrentUtilityProcesses && utilityProcessQueue.length > 0) {
|
|
const request = utilityProcessQueue.shift();
|
|
runUtilityProcess(request);
|
|
}
|
|
}
|
|
|
|
// Actually run a utility process
|
|
function runUtilityProcess(request) {
|
|
runningUtilityProcesses++;
|
|
|
|
try {
|
|
const processObj = Qt.createQmlObject(request.processString, root, request.name);
|
|
|
|
processObj.exited.connect(function (exitCode) {
|
|
processObj.destroy();
|
|
runningUtilityProcesses--;
|
|
request.onComplete(exitCode, processObj);
|
|
processUtilityQueue();
|
|
});
|
|
|
|
processObj.running = true;
|
|
} catch (e) {
|
|
Logger.e("ImageCache", "Failed to create " + request.name + ":", e);
|
|
runningUtilityProcesses--;
|
|
request.onError(e);
|
|
processUtilityQueue();
|
|
}
|
|
}
|
|
|
|
function getMtime(filePath, callback) {
|
|
const pathEsc = filePath.replace(/'/g, "'\\''");
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["stat", "-c", "%Y", "${pathEsc}"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
queueUtilityProcess({
|
|
name: "MtimeProcess",
|
|
processString: processString,
|
|
onComplete: function (exitCode, proc) {
|
|
const mtime = exitCode === 0 ? proc.stdout.text.trim() : "";
|
|
callback(mtime);
|
|
},
|
|
onError: function () {
|
|
callback("");
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkFileExists(filePath, callback) {
|
|
const pathEsc = filePath.replace(/'/g, "'\\''");
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["test", "-f", "${pathEsc}"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
queueUtilityProcess({
|
|
name: "FileExistsProcess",
|
|
processString: processString,
|
|
onComplete: function (exitCode) {
|
|
callback(exitCode === 0);
|
|
},
|
|
onError: function () {
|
|
callback(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getImageDimensions(filePath, callback) {
|
|
const pathEsc = filePath.replace(/'/g, "'\\''");
|
|
const processString = `
|
|
import QtQuick
|
|
import Quickshell.Io
|
|
Process {
|
|
command: ["identify", "-ping", "-format", "%w %h", "${pathEsc}[0]"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
}
|
|
`;
|
|
|
|
queueUtilityProcess({
|
|
name: "IdentifyProcess",
|
|
processString: processString,
|
|
onComplete: function (exitCode, proc) {
|
|
let width = 0, height = 0;
|
|
if (exitCode === 0) {
|
|
const parts = proc.stdout.text.trim().split(" ");
|
|
if (parts.length >= 2) {
|
|
width = parseInt(parts[0], 10) || 0;
|
|
height = parseInt(parts[1], 10) || 0;
|
|
}
|
|
}
|
|
callback(width, height);
|
|
},
|
|
onError: function () {
|
|
callback(0, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Cache Invalidation
|
|
// -------------------------------------------------
|
|
function invalidateThumbnail(sourcePath) {
|
|
Logger.i("ImageCache", "Invalidating thumbnail for:", sourcePath);
|
|
// Since cache keys include hash, we'd need to track mappings
|
|
// For simplicity, clear all thumbnails
|
|
clearThumbnails();
|
|
}
|
|
|
|
function invalidateLarge(sourcePath) {
|
|
Logger.i("ImageCache", "Invalidating large for:", sourcePath);
|
|
clearLarge();
|
|
}
|
|
|
|
function invalidateNotification(imageId) {
|
|
const path = notificationsDir + imageId + ".png";
|
|
Quickshell.execDetached(["rm", "-f", path]);
|
|
}
|
|
|
|
function invalidateAvatar(username) {
|
|
const path = contributorsDir + username + "_circular.png";
|
|
Quickshell.execDetached(["rm", "-f", path]);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Clear Cache Functions
|
|
// -------------------------------------------------
|
|
function clearAll() {
|
|
Logger.i("ImageCache", "Clearing all cache");
|
|
clearThumbnails();
|
|
clearLarge();
|
|
clearNotifications();
|
|
clearContributors();
|
|
}
|
|
|
|
function clearThumbnails() {
|
|
Logger.i("ImageCache", "Clearing thumbnails cache");
|
|
Quickshell.execDetached(["rm", "-rf", wpThumbDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
|
|
}
|
|
|
|
function clearLarge() {
|
|
Logger.i("ImageCache", "Clearing large cache");
|
|
Quickshell.execDetached(["rm", "-rf", wpLargeDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", wpLargeDir]);
|
|
}
|
|
|
|
function clearNotifications() {
|
|
Logger.i("ImageCache", "Clearing notifications cache");
|
|
Quickshell.execDetached(["rm", "-rf", notificationsDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
|
|
}
|
|
|
|
function clearContributors() {
|
|
Logger.i("ImageCache", "Clearing contributors cache");
|
|
Quickshell.execDetached(["rm", "-rf", contributorsDir]);
|
|
Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// ImageMagick Detection
|
|
// -------------------------------------------------
|
|
Process {
|
|
id: checkMagickProcess
|
|
command: ["sh", "-c", "command -v magick"]
|
|
running: false
|
|
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
|
|
onExited: function (exitCode) {
|
|
root.imageMagickAvailable = (exitCode === 0);
|
|
root.initialized = true;
|
|
if (root.imageMagickAvailable) {
|
|
Logger.i("ImageCache", "ImageMagick available");
|
|
} else {
|
|
Logger.w("ImageCache", "ImageMagick not found, using Qt fallback");
|
|
}
|
|
}
|
|
}
|
|
}
|