diff --git a/linux-wallpaperengine-controller/BarWidget.qml b/linux-wallpaperengine-controller/BarWidget.qml index 035420a05..41edec7f8 100644 --- a/linux-wallpaperengine-controller/BarWidget.qml +++ b/linux-wallpaperengine-controller/BarWidget.qml @@ -90,7 +90,7 @@ NIconButton { } else if (action === "start") { mainInstance?.reload(true); } else if (action === "settings") { - BarService.openPluginSettings(root.screen, pluginApi.manifest); + BarService.openPluginSettings(root.screen, pluginApi?.manifest); } } } diff --git a/linux-wallpaperengine-controller/Main.qml b/linux-wallpaperengine-controller/Main.qml index 38ecf1d89..eb8d3408e 100644 --- a/linux-wallpaperengine-controller/Main.qml +++ b/linux-wallpaperengine-controller/Main.qml @@ -2,7 +2,9 @@ import QtQuick import Quickshell import Quickshell.Io -import "helpers/ColorCacheHelpers.js" as ColorCacheHelpers +import "helpers/shared/ColorCacheHelpers.js" as ColorCacheHelpers +import "helpers/runtime/EngineHelpers.js" as EngineHelpers +import "helpers/runtime/WallpaperColorHelpers.js" as WallpaperColorHelpers import qs.Commons import qs.Services.UI @@ -11,6 +13,7 @@ import qs.Services.Theming Item { id: root + // Shared plugin API and runtime state. property var pluginApi: null property bool checkingEngine: true @@ -22,7 +25,7 @@ Item { property bool applyingWallpaperColors: false property string lastError: "" property string lastErrorDetails: "" - property string statusMessage: "" + property string lastRuntimeErrorKey: "" readonly property bool engineRunning: engineProcess.running || isApplying || pendingCommand.length > 0 property string lastScreenSetSignature: "" property bool scanningWallpapers: false @@ -32,11 +35,13 @@ Item { property var pendingWallpaperColorRequest: null property string pendingCachedWallpaperColorPath: "" property string pendingCachedWallpaperColorScreenName: "" + property bool pendingCachedWallpaperColorNotify: false property var pendingWallpaperColorReuseRequest: null property string wallpaperColorScreenName: "" property string wallpaperColorScaling: "fill" property string wallpaperColorRequestPath: "" property string wallpaperColorScreenshotPath: "" + property bool wallpaperColorNotify: false readonly property string activeColorMonitor: String(Settings.data.colorSchemes.monitorForColors || Quickshell.screens[0]?.name || "") readonly property bool wallpaperColorsEnabled: !!Settings.data.colorSchemes.useWallpaperColors readonly property bool wallpaperColorDarkMode: !!Settings.data.colorSchemes.darkMode @@ -47,13 +52,18 @@ Item { readonly property var cfg: pluginApi?.pluginSettings || ({}) readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) - // Initialization and persistence helpers. + // Initialization. Component.onCompleted: { Logger.i("LWEController", "Main initialized"); + + // Clean up leftover processes from previous sessions. + startupCleanupProcess.running = true; + lastScreenSetSignature = currentScreenSetSignature(); - scheduleCachedWallpaperColorsForMonitor("startup"); + startupWallpaperColorResyncTimer.restart(); } + // Settings persistence and recovery snapshots. function ensureSettingsRoot() { if (!pluginApi) { return; @@ -84,6 +94,10 @@ Item { return JSON.parse(JSON.stringify(value || ({}))); } + function currentWallpaperPathForScreen(screenName) { + return normalizedPath(getScreenConfig(screenName).path || ""); + } + function hasAnyScreenPathFrom(sourceScreens) { const screens = sourceScreens || ({}); const keys = Object.keys(screens); @@ -131,7 +145,7 @@ Item { pluginApi.pluginSettings.runtimeRecoveryPending = false; pluginApi.saveSettings(); - Logger.i("LWEController", "Saved last-known-good layout", "reason=", reason); + Logger.d("LWEController", "Saved last-known-good layout", "reason=", reason); return true; } @@ -152,7 +166,7 @@ Item { pluginApi.pluginSettings.runtimeRecoveryPending = false; pluginApi.saveSettings(); - Logger.i("LWEController", "Restored last-known-good layout", "reason=", reason); + Logger.d("LWEController", "Restored last-known-good layout", "reason=", reason); return true; } @@ -217,16 +231,17 @@ Item { readonly property bool defaultNoFullscreenPause: cfg.defaultNoFullscreenPause ?? defaults.defaultNoFullscreenPause ?? false readonly property bool defaultFullscreenPauseOnlyActive: cfg.defaultFullscreenPauseOnlyActive ?? defaults.defaultFullscreenPauseOnlyActive ?? false readonly property bool defaultAutoApply: cfg.autoApplyOnStartup ?? defaults.autoApplyOnStartup ?? true - readonly property string assetsDir: cfg.assetsDir ?? defaults.assetsDir ?? "" + readonly property int wallpaperScanCacheMinutes: { - const value = Number(cfg.wallpaperScanCacheMinutes ?? defaults.wallpaperScanCacheMinutes ?? 5); + const value = Number(cfg.wallpaperScanCacheMinutes ?? defaults.wallpaperScanCacheMinutes ?? 10); if (isNaN(value)) { - return 5; + return 10; } return Math.max(0, Math.floor(value)); } - // Screen and wallpaper configuration accessors. + + // Screen topology, path normalization, and persisted wallpaper accessors. function normalizedPath(path) { return Settings.preprocessPath(String(path || "")); } @@ -239,10 +254,6 @@ Item { } function wallpaperScanCacheValid() { - if (scanningWallpapers) { - return true; - } - if (wallpaperScanCacheMinutes <= 0) { return false; } @@ -303,6 +314,7 @@ Item { return parts.length > 0 ? String(parts[parts.length - 1] || "") : ""; } + // Wallpaper property storage helpers. function cloneWallpaperProperties(source) { const cloned = {}; const raw = source || ({}); @@ -364,37 +376,22 @@ Item { } } + // Wallpaper color synchronization helpers. function currentWallpaperColorMode() { return Settings.data.colorSchemes.darkMode ? "dark" : "light"; } - function syncWallpaperColorSource(screenName, screenshotPath) { - const normalizedScreenName = String(screenName || "").trim(); - const normalizedScreenshotPath = String(screenshotPath || "").trim(); - if (normalizedScreenName.length === 0 || normalizedScreenshotPath.length === 0) { - return; - } - - WallpaperService.changeWallpaper(normalizedScreenshotPath, normalizedScreenName, "dark"); - WallpaperService.changeWallpaper(normalizedScreenshotPath, normalizedScreenName, "light"); - } - - function applyWallpaperColorsFromScreenshot(screenName, screenshotPath) { + function applyWallpaperColorsFromScreenshot(screenshotPath) { if (String(screenshotPath || "").trim().length === 0) { return; } - syncWallpaperColorSource(screenName, screenshotPath); TemplateProcessor.processWallpaperColors(screenshotPath, currentWallpaperColorMode()); } function screenshotPathForWallpaper(path, screenName = "") { - return ColorCacheHelpers.screenshotPathForWallpaper( - Settings.cacheDir, - pluginApi?.manifest?.id || pluginApi?.pluginId || "linux-wallpaperengine-controller", - wallpaperIdFromPath(path), - screenName - ); + const pluginId = pluginApi?.manifest?.id || pluginApi?.pluginId || "linux-wallpaperengine-controller"; + return ColorCacheHelpers.screenshotPathForWallpaper(Settings.cacheDir, pluginId, wallpaperIdFromPath(path), screenName); } function wallpaperColorScreenshotEntry(screenName) { @@ -413,56 +410,34 @@ Item { ); } - function startWallpaperColorCapture(wallpaperPath, targetScreenName, targetScaling) { + function startWallpaperColorCapture(wallpaperPath, targetScreenName, targetScaling, notify = false) { const screenshotPath = screenshotPathForWallpaper(wallpaperPath, targetScreenName); const pluginDir = pluginApi?.pluginDir || ""; const scriptPath = pluginDir + "/scripts/capture-wallpaper-colors.sh"; - const command = [ - "bash", + const wallpaperProperties = getWallpaperProperties(wallpaperPath); + const resolvedScaling = targetScaling.length > 0 ? targetScaling : "fill"; + const command = WallpaperColorHelpers.buildCaptureCommand( scriptPath, screenshotPath, - "linux-wallpaperengine" - ]; - const maybeAssetsDir = normalizedPath(assetsDir); - const wallpaperProperties = getWallpaperProperties(wallpaperPath); - - if (maybeAssetsDir.length > 0) { - command.push("--assets-dir"); - command.push(maybeAssetsDir); - } - - command.push("--fps"); - command.push(String(defaultFps)); - command.push("--clamp"); - command.push(String(defaultClamp || "clamp")); - command.push("--screen-root"); - command.push(targetScreenName); - command.push("--bg"); - command.push(wallpaperPath); - command.push("--scaling"); - command.push(targetScaling.length > 0 ? targetScaling : "fill"); - command.push("--screenshot"); - command.push(screenshotPath); - - for (const propertyKey of Object.keys(wallpaperProperties)) { - const propertyValue = wallpaperProperties[propertyKey]; - if (propertyValue === undefined || propertyValue === null || String(propertyKey || "").trim().length === 0) { - continue; - } - command.push("--set-property"); - command.push(String(propertyKey) + "=" + String(propertyValue)); - } + "", + defaultFps, + defaultClamp, + targetScreenName, + wallpaperPath, + resolvedScaling, + wallpaperProperties + ); applyingWallpaperColors = true; wallpaperColorRequestPath = wallpaperPath; wallpaperColorScreenshotPath = screenshotPath; wallpaperColorScreenName = targetScreenName; - wallpaperColorScaling = targetScaling.length > 0 ? targetScaling : "fill"; + wallpaperColorScaling = resolvedScaling; + wallpaperColorNotify = !!notify; wallpaperColorProcess.command = command; wallpaperColorProcess.running = true; Logger.i("LWEController", "Generating screenshot for wallpaper color extraction", "path=", wallpaperPath, "screen=", targetScreenName, "scaling=", wallpaperColorScaling, "output=", screenshotPath); - ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsGenerating"), "palette"); } function saveWallpaperColorScreenshot(screenName, screenshotPath, wallpaperPath, scaling) { @@ -471,16 +446,22 @@ Item { } ensureSettingsRoot(); - pluginApi.pluginSettings.wallpaperColorScreenshots[screenName] = { - "path": screenshotPath, - "wallpaperPath": wallpaperPath, - "scaling": scaling, - "updatedAt": Date.now() - }; + pluginApi.pluginSettings.wallpaperColorScreenshots[screenName] = WallpaperColorHelpers.buildScreenshotCacheEntry( + screenshotPath, + wallpaperPath, + scaling + ); pluginApi.saveSettings(); + + const pluginDir = pluginApi?.pluginDir || ""; + const scriptPath = pluginDir + "/scripts/update-noctalia-wallpapers-cache.sh"; + Quickshell.execDetached(["bash", scriptPath, screenName, screenshotPath]); } function scheduleCachedWallpaperColorsForMonitor(reason = "") { + pendingCachedWallpaperColorPath = ""; + pendingCachedWallpaperColorScreenName = ""; + if (!wallpaperColorsEnabled) { return; } @@ -490,39 +471,47 @@ Item { return; } - const entry = wallpaperColorScreenshotEntry(screenName); - const screenshotPath = normalizedPath(entry?.path || ""); - if (screenshotPath.length === 0) { - Logger.d("LWEController", "No cached wallpaper color screenshot for active monitor", "screen=", screenName, "reason=", reason); + const screenCfg = getScreenConfig(screenName); + const request = WallpaperColorHelpers.buildActiveMonitorSyncRequest(screenName, screenCfg, defaultScaling, normalizedPath); + if (!request) { + Logger.d("LWEController", "Skip wallpaper color sync: no configured wallpaper for active monitor", "screen=", screenName, "reason=", reason); return; } - pendingCachedWallpaperColorPath = screenshotPath; - pendingCachedWallpaperColorScreenName = screenName; - const pluginDir = pluginApi?.pluginDir || ""; - const scriptPath = pluginDir + "/scripts/check-file-exists.sh"; - cachedWallpaperColorSyncCheckProcess.command = ["bash", scriptPath, screenshotPath]; - cachedWallpaperColorSyncCheckProcess.running = true; - Logger.d("LWEController", "Scheduled cached wallpaper color sync", "screen=", screenName, "path=", screenshotPath, "reason=", reason); + Logger.d("LWEController", "Scheduling wallpaper color sync for current active monitor wallpaper", "screen=", request.screenName, "path=", request.path, "scaling=", request.scaling, "reason=", reason); + scheduleWallpaperColorsFromPath(request.path, request); } function applyWallpaperColorsFromPath(path, options = null) { - const wallpaperPath = normalizedPath(path); - const requestOptions = options || ({}); - const targetScreenName = String(requestOptions.screenName || Quickshell.screens[0]?.name || "").trim(); - const targetScaling = String(requestOptions.scaling || defaultScaling || "fill").trim(); + const request = WallpaperColorHelpers.normalizeWallpaperColorRequest( + path, + options, + defaultScaling, + Quickshell.screens[0]?.name || "", + normalizedPath + ); + const wallpaperPath = request.path; + const targetScreenName = request.screenName; + const targetScaling = request.scaling; + const notify = request.notify; if (!engineAvailable) { - ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsEngineUnavailable"), "alert-circle"); + if (notify) { + ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsEngineUnavailable"), "alert-circle"); + } return; } if (wallpaperPath.length === 0) { - ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsNoSelection"), "alert-circle"); + if (notify) { + ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsNoSelection"), "alert-circle"); + } return; } if (targetScreenName.length === 0) { - ToastService.showError(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsFailed"), "alert-circle"); + if (notify) { + ToastService.showError(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsFailed"), "alert-circle"); + } return; } @@ -535,37 +524,50 @@ Item { const cachedPath = normalizedPath(entry?.path || ""); const pluginDir = pluginApi?.pluginDir || ""; const scriptPath = pluginDir + "/scripts/check-file-exists.sh"; - pendingWallpaperColorReuseRequest = { - "wallpaperPath": wallpaperPath, - "screenName": targetScreenName, - "scaling": targetScaling, - "screenshotPath": cachedPath - }; + pendingWallpaperColorReuseRequest = WallpaperColorHelpers.buildReuseCheckRequest( + targetScreenName, + wallpaperPath, + targetScaling, + cachedPath, + notify + ); reusedWallpaperColorCheckProcess.command = ["bash", scriptPath, cachedPath]; reusedWallpaperColorCheckProcess.running = true; return; } - startWallpaperColorCapture(wallpaperPath, targetScreenName, targetScaling); + startWallpaperColorCapture(wallpaperPath, targetScreenName, targetScaling, notify); } function scheduleWallpaperColorsFromPath(path, options = null) { - const wallpaperPath = normalizedPath(path); - if (wallpaperPath.length === 0) { + const request = WallpaperColorHelpers.normalizeWallpaperColorRequest( + path, + options, + defaultScaling, + Quickshell.screens[0]?.name || "", + normalizedPath + ); + if (request.path.length === 0) { return; } - pendingWallpaperColorRequest = { - "path": wallpaperPath, - "screenName": String(options?.screenName || Quickshell.screens[0]?.name || ""), - "scaling": String(options?.scaling || defaultScaling || "fill") - }; + pendingCachedWallpaperColorPath = ""; + pendingCachedWallpaperColorScreenName = ""; + pendingCachedWallpaperColorNotify = false; + + pendingWallpaperColorRequest = request; wallpaperColorStartTimer.restart(); - Logger.d("LWEController", "Scheduled wallpaper color extraction", "path=", wallpaperPath, "screen=", pendingWallpaperColorRequest.screenName, "scaling=", pendingWallpaperColorRequest.scaling); + Logger.d("LWEController", "Scheduled wallpaper color extraction", "path=", pendingWallpaperColorRequest.path, "screen=", pendingWallpaperColorRequest.screenName, "scaling=", pendingWallpaperColorRequest.scaling); } + // Wallpaper source scanning and cache refresh. function refreshWallpaperCache(force = false, showToast = false) { const folderPath = Settings.preprocessPath(String(cfg.wallpapersFolder ?? defaults.wallpapersFolder ?? "")).trim(); + if (scanningWallpapers && !wallpaperScanProcess.running) { + Logger.w("LWEController", "Reset stale wallpaper scanning state before refresh"); + scanningWallpapers = false; + } + if (folderPath.length === 0) { cachedWallpaperItems = []; wallpapersFolderAccessible = false; @@ -578,7 +580,14 @@ Item { return; } + if (wallpaperScanProcess.running) { + Logger.d("LWEController", "Wallpaper scan already in progress"); + return; + } + if (!force && wallpaperScanCacheValid()) { + scanningWallpapers = false; + wallpaperScanShowToast = false; Logger.d("LWEController", "Wallpaper cache reused", "count=", cachedWallpaperItems.length, "ageMs=", Date.now() - lastWallpaperScanAt); return; } @@ -589,14 +598,12 @@ Item { Logger.i("LWEController", force ? "Refreshing wallpaper cache" : "Scanning wallpapers for cache", folderPath); scanningWallpapers = true; wallpaperScanShowToast = showToast; + // Mode is no longer passed as argument since we eliminated multi-mode. wallpaperScanProcess.command = ["bash", scriptPath, folderPath]; wallpaperScanProcess.running = true; - if (showToast) { - ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.refreshingWallpapers"), "refresh"); - } } - // Wallpaper application and persistence. + // Wallpaper application and persisted runtime option updates. function setScreenWallpaperWithOptions(screenName, path, options) { if (!pluginApi) { return; @@ -618,7 +625,7 @@ Item { pluginApi.pluginSettings.screens[screenName].scaling = resolvedScaling; } if (resolvedClamp.length > 0) { - pluginApi.pluginSettings.defaultClamp = resolvedClamp; + pluginApi.pluginSettings.screens[screenName].clamp = resolvedClamp; } if (options?.volume !== undefined) { @@ -656,7 +663,7 @@ Item { pluginApi.saveSettings(); - restartEngine(); + restartEngine(resolveRuntimeOverrides(options)); } function clearScreenWallpaper(screenName) { @@ -711,15 +718,14 @@ Item { if (resolvedScaling.length > 0) { pluginApi.pluginSettings.screens[screen.name].scaling = resolvedScaling; } + if (resolvedClamp.length > 0) { + pluginApi.pluginSettings.screens[screen.name].clamp = resolvedClamp; + } if (options?.customProperties !== undefined) { setWallpaperProperties(path, options.customProperties); } } - if (resolvedClamp.length > 0) { - pluginApi.pluginSettings.defaultClamp = resolvedClamp; - } - if (hasResolvedVolume) { pluginApi.pluginSettings.defaultVolume = resolvedVolume; } @@ -742,53 +748,16 @@ Item { clearLegacyRuntimeOptionsForAllScreens(); pluginApi.saveSettings(); - restartEngine(); + restartEngine(resolveRuntimeOverrides(options)); } - // Runtime error handling. + // Runtime error capture and recovery hints. function extractRuntimeError(stderrText) { - const text = (stderrText || "").trim(); - if (text.length === 0) { - return ""; - } - - const lower = text.toLowerCase(); - - if (lower.indexOf("cannot find a valid assets folder") !== -1) { - return pluginApi?.tr("main.error.assetsMissing"); - } - - if (lower.indexOf("at least one background id must be specified") !== -1) { - return pluginApi?.tr("main.error.noBackground"); - } - - if (lower.indexOf("opengl") !== -1 || lower.indexOf("glfw") !== -1) { - return pluginApi?.tr("main.error.opengl"); - } - - const lines = text.split(/\r?\n/) - .map(line => (line || "").trim()) - .filter(line => line.length > 0); - - if (lines.length === 0) { - return ""; - } - - let summary = lines[0]; - for (const line of lines) { - const normalized = line.toLowerCase(); - if (normalized.indexOf("error") !== -1 || normalized.indexOf("failed") !== -1) { - summary = line; - break; - } - } - - const maxLength = 220; - if (summary.length > maxLength) { - summary = summary.substring(0, maxLength) + "..."; - } - - return summary; + return EngineHelpers.extractRuntimeError(stderrText, { + assetsMissing: pluginApi?.tr("main.error.assetsMissing"), + noBackground: pluginApi?.tr("main.error.noBackground"), + opengl: pluginApi?.tr("main.error.opengl") + }); } function setRuntimeErrorFromStderr(stderrText) { @@ -807,6 +776,36 @@ Item { return true; } + function clearRuntimeErrorState() { + lastError = ""; + lastErrorDetails = ""; + lastRuntimeErrorKey = ""; + } + + function logCapturedRuntimeError(stage, exitCode = null, exitStatus = null) { + const summary = String(lastError || "").trim(); + const details = String(lastErrorDetails || "").trim(); + if (summary.length === 0 && details.length === 0) { + return false; + } + + const logKey = stage + "|" + summary + "|" + details; + if (logKey === lastRuntimeErrorKey) { + return true; + } + + lastRuntimeErrorKey = logKey; + Logger.e( + "LWEController", + "runtime-error", + "stage=", stage, + "exitCode=", exitCode === null ? "-" : exitCode, + "exitStatus=", exitStatus === null ? "-" : exitStatus, + "summary=", summary + ); + return true; + } + function markErrorAsRecovered() { const hintRaw = pluginApi?.tr("main.error.autoRecovered"); if (hintRaw === undefined || hintRaw === null) { @@ -826,101 +825,41 @@ Item { lastError = current + " (" + hint + ")"; } - // Command construction and engine lifecycle. - function buildCommand() { - const command = ["linux-wallpaperengine"]; - let firstPath = ""; - const appendedWallpaperIds = {}; - let runtimeOptions = { - volume: defaultVolume, - muted: defaultMuted, - audioReactiveEffects: defaultAudioReactiveEffects, - noAutomute: defaultNoAutomute, - disableMouse: defaultDisableMouse, - disableParallax: defaultDisableParallax - }; - - command.push("--fps"); - command.push(String(defaultFps)); - - const runtimeClamp = String(defaultClamp || "clamp").trim(); - if (runtimeClamp.length > 0) { - command.push("--clamp"); - command.push(runtimeClamp); - } - - if (runtimeOptions.muted) { - command.push("--silent"); - } else { - command.push("--volume"); - command.push(String(runtimeOptions.volume)); - } - - if (!runtimeOptions.audioReactiveEffects) { - command.push("--no-audio-processing"); - } - - if (runtimeOptions.noAutomute) { - command.push("--noautomute"); - } - - if (runtimeOptions.disableMouse) { - command.push("--disable-mouse"); - } - - if (runtimeOptions.disableParallax) { - command.push("--disable-parallax"); - } - - if (defaultNoFullscreenPause) { - command.push("--no-fullscreen-pause"); - } - - if (defaultFullscreenPauseOnlyActive) { - command.push("--fullscreen-pause-only-active"); - } - - const maybeAssetsDir = normalizedPath(assetsDir); - if (maybeAssetsDir.length > 0) { - command.push("--assets-dir"); - command.push(maybeAssetsDir); - } - - for (const screen of Quickshell.screens) { - const screenCfg = getScreenConfig(screen.name); - const path = normalizedPath(screenCfg.path); - if (!path || path.length === 0) { - continue; - } - - if (firstPath.length === 0) { - firstPath = path; - } - - command.push("--screen-root"); - command.push(screen.name); - command.push("--bg"); - command.push(path); - - command.push("--scaling"); - command.push(String(screenCfg.scaling)); - - const wallpaperId = wallpaperIdFromPath(path); - if (wallpaperId.length > 0 && !appendedWallpaperIds[wallpaperId]) { - const customProperties = getWallpaperProperties(path); - for (const propertyKey of Object.keys(customProperties)) { - const propertyValue = customProperties[propertyKey]; - if (propertyValue === undefined || propertyValue === null || String(propertyKey || "").trim().length === 0) { - continue; - } - command.push("--set-property"); - command.push(String(propertyKey) + "=" + String(propertyValue)); - } - appendedWallpaperIds[wallpaperId] = true; - } - } + // Engine command construction and lifecycle orchestration. + function buildCommand(runtimeOverrides = null) { + const overrides = runtimeOverrides || ({}); + return EngineHelpers.buildEngineCommand({ + defaultFps: defaultFps, + defaultClamp: overrides.defaultClamp ?? defaultClamp, + defaultVolume: overrides.defaultVolume ?? defaultVolume, + defaultMuted: overrides.defaultMuted ?? defaultMuted, + defaultAudioReactiveEffects: overrides.defaultAudioReactiveEffects ?? defaultAudioReactiveEffects, + defaultNoAutomute: overrides.defaultNoAutomute ?? defaultNoAutomute, + defaultDisableMouse: overrides.defaultDisableMouse ?? defaultDisableMouse, + defaultDisableParallax: overrides.defaultDisableParallax ?? defaultDisableParallax, + defaultNoFullscreenPause: defaultNoFullscreenPause, + defaultFullscreenPauseOnlyActive: defaultFullscreenPauseOnlyActive, + defaultScaling: defaultScaling, + screens: Quickshell.screens, + getScreenConfig: getScreenConfig, + normalizePath: normalizedPath, + wallpaperIdFromPath: wallpaperIdFromPath, + getWallpaperProperties: getWallpaperProperties + }); + } - return command; + function resolveRuntimeOverrides(options = null) { + const opts = options || ({}); + const resolvedVolumeRaw = Number(opts.volume); + return { + defaultClamp: String(opts.clamp || defaultClamp), + defaultVolume: isNaN(resolvedVolumeRaw) ? defaultVolume : Math.max(0, Math.min(100, Math.floor(resolvedVolumeRaw))), + defaultMuted: opts.muted === undefined ? defaultMuted : !!opts.muted, + defaultAudioReactiveEffects: opts.audioReactiveEffects === undefined ? defaultAudioReactiveEffects : !!opts.audioReactiveEffects, + defaultNoAutomute: opts.noAutomute === undefined ? defaultNoAutomute : !!opts.noAutomute, + defaultDisableMouse: opts.disableMouse === undefined ? defaultDisableMouse : !!opts.disableMouse, + defaultDisableParallax: opts.disableParallax === undefined ? defaultDisableParallax : !!opts.disableParallax + }; } function stopAll(showToast = false) { @@ -940,7 +879,6 @@ Item { } isApplying = false; - statusMessage = pluginApi?.tr("main.status.stopped"); if (showToast) { ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.stopped"), "player-stop"); } @@ -948,12 +886,12 @@ Item { function startEngineWithCommand(command) { if (!engineAvailable) { - Logger.w("LWEController", "Skip start: engine unavailable"); + Logger.d("LWEController", "Skip start: engine unavailable"); return; } if (!command || command.length <= 1) { - Logger.w("LWEController", "Skip start: empty command"); + Logger.d("LWEController", "Skip start: empty command"); stopAll(); return; } @@ -961,10 +899,8 @@ Item { Logger.d("LWEController", "Starting engine command", JSON.stringify(command)); if (!recoveryInProgress) { - lastError = ""; - lastErrorDetails = ""; + clearRuntimeErrorState(); } - statusMessage = pluginApi?.tr("main.status.starting"); isApplying = true; engineProcess.command = command; @@ -972,21 +908,21 @@ Item { stableRunTimer.restart(); } - function restartEngine() { + function restartEngine(runtimeOverrides = null) { if (!engineAvailable) { - Logger.w("LWEController", "Skip restart: engine unavailable"); + Logger.d("LWEController", "Skip restart: engine unavailable"); return; } if (!hasAnyConfiguredWallpaper()) { - Logger.i("LWEController", "Skip restart: no configured wallpaper; stopping engine"); + Logger.d("LWEController", "Skip restart: no configured wallpaper; stopping engine"); stopAll(); return; } - const command = buildCommand(); + const command = buildCommand(runtimeOverrides); if (!command || command.length <= 1) { - Logger.w("LWEController", "Restart resolved to empty command; stopping engine"); + Logger.d("LWEController", "Restart resolved to empty command; stopping engine"); stopAll(); return; } @@ -1009,10 +945,8 @@ Item { function reload(showToast = false) { if (!hasAnyConfiguredWallpaper()) { - lastError = ""; - lastErrorDetails = ""; - statusMessage = pluginApi?.tr("main.status.ready"); - Logger.i("LWEController", "Reload skipped: no configured wallpaper paths"); + clearRuntimeErrorState(); + Logger.d("LWEController", "Reload skipped: no configured wallpaper paths"); if (showToast) { ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.reloadSkippedNoWallpaper"), "alert-circle"); } @@ -1025,7 +959,7 @@ Item { } } - // External command and IPC integration. + // External processes. Process { id: wallpaperScanProcess @@ -1051,10 +985,14 @@ Item { const id = parts.length > 5 ? parts[5] : String(path || "").split("/").pop(); const type = parts.length > 6 ? parts[6] : "unknown"; const resolution = parts.length > 7 ? parts[7] : "unknown"; - const sizeMtime = parts.length > 8 ? parts[8] : "0:0"; + const hasEmbeddedAudio = parts.length > 8 ? parts[8] === "1" : false; + const hasAudioReactive = parts.length > 9 ? parts[9] === "1" : false; + const sizeMtime = parts.length > 10 ? parts[10] : "0:0"; const sizeParts = String(sizeMtime).split(":"); const bytes = sizeParts.length > 0 ? Number(sizeParts[0]) : 0; const mtime = sizeParts.length > 1 ? Number(sizeParts[1]) : 0; + const approved = parts.length > 11 ? parts[11] === "1" : false; + const description = parts.length > 12 ? String(parts[12] || "") : ""; if (path.length > 0) { parsed.push({ @@ -1063,11 +1001,15 @@ Item { thumb: thumb, motionPreview: motionPreview, dynamic: dynamic, + hasEmbeddedAudio: hasEmbeddedAudio, + hasAudioReactive: hasAudioReactive, id: id, type: type, resolution: resolution, bytes: bytes, - mtime: mtime + mtime: mtime, + approved: approved, + description: description }); } } @@ -1086,11 +1028,8 @@ Item { root.wallpaperScanShowToast = false; if (!root.wallpapersFolderAccessible) { - if (stderrText.length > 0) { - Logger.e("LWEController", "Wallpaper scan failed", "folder=", Settings.preprocessPath(String(cfg.wallpapersFolder ?? defaults.wallpapersFolder ?? "")), "exitCode=", exitCode, "stderr=", stderrText); - } else { - Logger.e("LWEController", "Wallpaper scan failed", "exitCode=", exitCode); - } + const msg = stderrText.length > 0 ? "Wallpaper scan failed, stderr=" + stderrText : "Wallpaper scan failed"; + Logger.e("LWEController", msg, "exitCode=", exitCode); } Logger.i("LWEController", "Wallpaper cache updated", "count=", parsed.length, "exitCode=", exitCode); @@ -1114,13 +1053,11 @@ Item { if (!root.engineAvailable) { root.lastError = root.pluginApi?.tr("main.error.notInstalled"); root.lastErrorDetails = ""; - root.statusMessage = root.pluginApi?.tr("main.status.unavailable"); + root.lastRuntimeErrorKey = ""; Logger.e("LWEController", "linux-wallpaperengine binary not found in PATH"); return; } - root.statusMessage = root.pluginApi?.tr("main.status.ready"); - root.refreshWallpaperCache(false, false); root.recoverPendingLayoutOnStartup(); @@ -1156,19 +1093,16 @@ Item { return; } - root.statusMessage = root.pluginApi?.tr("main.status.stopped"); return; } if (exitCode !== 0 || exitStatus !== Process.NormalExit) { if (root.setRuntimeErrorFromStderr(stderr.text)) { - Logger.e("LWEController", "Engine runtime error", root.lastError); + root.logCapturedRuntimeError("engine-exit", exitCode, exitStatus); } root.tryAutoRecoverFromRuntimeError("runtime-crash"); - root.statusMessage = root.pluginApi?.tr("main.status.crashed"); } else { root.recoveryInProgress = false; - root.statusMessage = root.pluginApi?.tr("main.status.stopped"); } } @@ -1180,9 +1114,7 @@ Item { return; } - if (root.setRuntimeErrorFromStderr(text)) { - Logger.w("LWEController", "Engine stderr", root.lastError); - } + root.setRuntimeErrorFromStderr(text); } } } @@ -1215,6 +1147,8 @@ Item { const requestPath = root.wallpaperColorRequestPath; const screenshotPath = root.wallpaperColorScreenshotPath; const screenName = root.wallpaperColorScreenName; + const appliedScaling = root.wallpaperColorScaling; + const notify = root.wallpaperColorNotify; const stderrText = String(stderr.text || "").trim(); root.applyingWallpaperColors = false; @@ -1222,27 +1156,54 @@ Item { root.wallpaperColorScreenshotPath = ""; root.wallpaperColorScreenName = ""; root.wallpaperColorScaling = "fill"; + root.wallpaperColorNotify = false; if (exitCode !== 0) { - Logger.w("LWEController", "Wallpaper screenshot generation failed", "path=", requestPath, "screen=", screenName, "exitCode=", exitCode, "stderr=", stderrText); - ToastService.showError(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsFailed"), "alert-circle"); + const msg = stderrText.length > 0 ? "Wallpaper screenshot generation failed, stderr=" + stderrText : "Wallpaper screenshot generation failed"; + Logger.w("LWEController", msg, "path=", requestPath, "screen=", screenName, "exitCode=", exitCode); + if (notify) { + ToastService.showError(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsFailed"), "alert-circle"); + } return; } - saveWallpaperColorScreenshot(screenName, screenshotPath, requestPath, root.wallpaperColorScaling); + saveWallpaperColorScreenshot(screenName, screenshotPath, requestPath, appliedScaling); if (wallpaperColorsEnabled && screenName === activeColorMonitor) { - root.applyWallpaperColorsFromScreenshot(screenName, screenshotPath); + root.applyWallpaperColorsFromScreenshot(screenshotPath); Logger.i("LWEController", "Wallpaper screenshot generated and applied for active color monitor", "path=", requestPath, "screen=", screenName, "screenshot=", screenshotPath); - ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsApplied"), "palette"); + if (notify) { + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsApplied"), "palette"); + } return; } Logger.i("LWEController", "Wallpaper screenshot cached for color extraction", "path=", requestPath, "screen=", screenName, "screenshot=", screenshotPath); - ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsCached"), "palette"); + if (notify) { + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsCached"), "palette"); + } } } + // Startup cleanup: kill any orphaned wallpaper engine processes from previous sessions. + Process { + id: startupCleanupProcess + running: false + + command: { + const pluginDir = root.pluginApi?.pluginDir || ""; + const scriptPath = pluginDir + "/scripts/force-stop-engine.sh"; + return ["bash", scriptPath]; + } + + onExited: function (exitCode) { + Logger.d("LWEController", "Startup cleanup finished", "exitCode=", exitCode); + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + Process { id: reusedWallpaperColorCheckProcess running: false @@ -1258,16 +1219,18 @@ Item { Logger.i("LWEController", "Reusing cached wallpaper color screenshot", "path=", request.wallpaperPath, "screen=", request.screenName, "scaling=", request.scaling, "screenshot=", request.screenshotPath); if (root.wallpaperColorsEnabled && request.screenName === root.activeColorMonitor) { - root.applyWallpaperColorsFromScreenshot(request.screenName, request.screenshotPath); - ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsApplied"), "palette"); - } else { + root.applyWallpaperColorsFromScreenshot(request.screenshotPath); + if (request.notify) { + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsApplied"), "palette"); + } + } else if (request.notify) { ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.wallpaperColorsCached"), "palette"); } return; } Logger.w("LWEController", "Cached wallpaper color screenshot missing; regenerating", "path=", request.wallpaperPath, "screen=", request.screenName, "scaling=", request.scaling, "screenshot=", request.screenshotPath); - root.startWallpaperColorCapture(request.wallpaperPath, request.screenName, request.scaling); + root.startWallpaperColorCapture(request.wallpaperPath, request.screenName, request.scaling, !!request.notify); } stdout: StdioCollector {} @@ -1281,14 +1244,17 @@ Item { onExited: function (exitCode) { const screenshotPath = root.pendingCachedWallpaperColorPath; const screenName = root.pendingCachedWallpaperColorScreenName; + const notify = root.pendingCachedWallpaperColorNotify; if (exitCode !== 0 || screenshotPath.length === 0) { Logger.w("LWEController", "Cached wallpaper color screenshot missing for active monitor", "screen=", screenName, "path=", screenshotPath, "exitCode=", exitCode); root.pendingCachedWallpaperColorPath = ""; root.pendingCachedWallpaperColorScreenName = ""; + root.pendingCachedWallpaperColorNotify = false; return; } + root.pendingCachedWallpaperColorNotify = notify; cachedWallpaperColorSyncTimer.restart(); } @@ -1296,6 +1262,7 @@ Item { stderr: StdioCollector {} } + // Timers and deferred follow-up work. Timer { id: wallpaperColorStartTimer interval: 1500 @@ -1313,20 +1280,33 @@ Item { Timer { id: cachedWallpaperColorSyncTimer interval: 250 + repeat: false onTriggered: { const screenshotPath = root.pendingCachedWallpaperColorPath; const screenName = root.pendingCachedWallpaperColorScreenName; root.pendingCachedWallpaperColorPath = ""; root.pendingCachedWallpaperColorScreenName = ""; + root.pendingCachedWallpaperColorNotify = false; if (screenshotPath.length === 0 || !root.wallpaperColorsEnabled) { return; } Logger.i("LWEController", "Applying cached wallpaper colors for active monitor", "screen=", screenName || root.activeColorMonitor, "path=", screenshotPath); - root.applyWallpaperColorsFromScreenshot(screenName || root.activeColorMonitor, screenshotPath); + root.applyWallpaperColorsFromScreenshot(screenshotPath); } } + Timer { + id: startupWallpaperColorResyncTimer + interval: 2200 + repeat: false + + onTriggered: { + root.scheduleCachedWallpaperColorsForMonitor("startup-final-resync"); + } + } + + // IPC entrypoints. IpcHandler { target: "plugin:linux-wallpaperengine-controller" @@ -1340,11 +1320,11 @@ Item { function apply(screenName: string, bgPath: string) { if (!screenName || !bgPath) { - Logger.w("LWEController", "IPC apply ignored due to invalid args", screenName, bgPath); + Logger.w("LWEController", "IPC apply ignored due to invalid args", "screenName=", screenName, "bgPath=", bgPath); return; } - Logger.i("LWEController", "IPC apply", screenName, bgPath); + Logger.i("LWEController", "IPC apply", "screenName=", screenName, "bgPath=", bgPath); root.setScreenWallpaper(screenName, bgPath); } @@ -1370,6 +1350,7 @@ Item { } } + // Shell event connections. Connections { target: Quickshell @@ -1383,6 +1364,7 @@ Item { onWallpaperColorDarkModeChanged: scheduleCachedWallpaperColorsForMonitor("dark-mode-changed") onWallpaperColorGenerationMethodChanged: scheduleCachedWallpaperColorsForMonitor("generation-method-changed") + // Stability and topology debounce timers. Timer { id: stableRunTimer interval: 2500 @@ -1417,4 +1399,39 @@ Item { root.restartEngine(); } } + + // Cleanup on shell shutdown. + Component.onDestruction: { + Logger.i("LWEController", "Shell shutting down, cleaning up wallpaper engine processes"); + + pendingCommand = []; + stopRequested = true; + + if (engineProcess.running) + engineProcess.running = false; + + if (wallpaperScanProcess.running) + wallpaperScanProcess.running = false; + if (wallpaperColorProcess.running) + wallpaperColorProcess.running = false; + if (reusedWallpaperColorCheckProcess.running) + reusedWallpaperColorCheckProcess.running = false; + if (cachedWallpaperColorSyncCheckProcess.running) + cachedWallpaperColorSyncCheckProcess.running = false; + + if (!forceStopProcess.running) + forceStopProcess.running = true; + + stableRunTimer.stop(); + screenTopologyRestartDebounce.stop(); + wallpaperColorStartTimer.stop(); + cachedWallpaperColorSyncTimer.stop(); + startupWallpaperColorResyncTimer.stop(); + + isApplying = false; + scanningWallpapers = false; + applyingWallpaperColors = false; + + Logger.i("LWEController", "Cleanup complete"); + } } diff --git a/linux-wallpaperengine-controller/Panel.qml b/linux-wallpaperengine-controller/Panel.qml index 4e8314f65..00e1da09d 100644 --- a/linux-wallpaperengine-controller/Panel.qml +++ b/linux-wallpaperengine-controller/Panel.qml @@ -1,7 +1,5 @@ import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import QtMultimedia import Quickshell import Quickshell.Io @@ -9,9 +7,13 @@ import qs.Commons import qs.Services.UI import qs.Widgets -import "components" -import "helpers/WallpaperMetaHelpers.js" as WallpaperMetaHelpers -import "helpers/PropertyHelpers.js" as PropertyHelpers +import "components/panel" +import "components/wallpaper" +import "helpers/panel/BadgeHelpers.js" as BadgeHelpers +import "helpers/panel/WallpaperFilterHelpers.js" as WallpaperFilterHelpers +import "helpers/panel/WallpaperMetaHelpers.js" as WallpaperMetaHelpers +import "helpers/panel/WallpaperUiHelpers.js" as WallpaperUiHelpers +import "helpers/panel/PropertyHelpers.js" as PropertyHelpers Item { id: root @@ -29,8 +31,6 @@ Item { property real contentPreferredHeight: 860 * Style.uiScaleRatio readonly property bool allowAttach: true - readonly property bool panelAnchorHorizontalCenter: false - readonly property bool panelAnchorVerticalCenter: false // Panel state and current selection. readonly property string wallpapersFolder: cfg.wallpapersFolder ?? defaults.wallpapersFolder ?? "" @@ -38,6 +38,7 @@ Item { property string selectedScreenName: pluginApi?.panelOpenScreen?.name ?? "" property string selectedPath: "" property string pendingPath: "" + property string lastPersistedPath: "" property string selectedScaling: "fill" property string selectedClamp: "clamp" property int selectedVolume: 100 @@ -50,7 +51,6 @@ Item { readonly property bool scanningWallpapers: mainInstance?.scanningWallpapers ?? false property bool loadingWallpaperProperties: false property bool scanningCompatibility: false - property bool pendingCompatibilityScan: false readonly property bool folderAccessible: mainInstance?.wallpapersFolderAccessible ?? true property string searchText: "" @@ -59,24 +59,28 @@ Item { property string sortMode: "name" property bool sortAscending: true property int currentPage: 0 - property int pageSize: 24 + readonly property int pageSize: Math.max(1, Number(cfg.panelPageSize ?? defaults.panelPageSize ?? 24) || 24) readonly property bool singleScreenMode: Quickshell.screens.length <= 1 property bool applyAllDisplays: !singleScreenMode && root._applyAllDisplays property bool _applyAllDisplays: true + property bool sidebarVisible: true + readonly property var defaultBadgeOrder: BadgeHelpers.normalizedDefaultOrder(defaults.badgeOrder) + readonly property var defaultBadgeEnabled: BadgeHelpers.normalizedDefaultEnabled(defaults.badgeEnabled) + readonly property var badgeOrder: BadgeHelpers.normalizeBadgeOrder(cfg.badgeOrder, defaultBadgeOrder) + readonly property var badgeEnabled: BadgeHelpers.normalizeBadgeEnabled(cfg.badgeEnabled, defaultBadgeEnabled) + readonly property var visibleBadgeOrder: BadgeHelpers.filterVisibleBadgeOrder(badgeOrder, badgeEnabled, defaultBadgeEnabled) + readonly property bool showSidebarDescription: cfg.showSidebarDescription ?? defaults.showSidebarDescription ?? false property bool applyTargetExpanded: false property bool filterDropdownOpen: false - property bool resolutionDropdownOpen: false property bool sortDropdownOpen: false property bool errorDetailsExpanded: false property real filterDropdownX: 0 property real filterDropdownY: 0 property real filterDropdownWidth: 220 * Style.uiScaleRatio - property real resolutionDropdownX: 0 - property real resolutionDropdownY: 0 - property real resolutionDropdownWidth: 220 * Style.uiScaleRatio property real sortDropdownX: 0 property real sortDropdownY: 0 property real sortDropdownWidth: 220 * Style.uiScaleRatio + property bool panelInitialized: false // Data models and derived UI state. property var screenModel: [] @@ -88,44 +92,32 @@ Item { property var wallpaperPropertyValues: ({}) property string wallpaperPropertyError: "" property string wallpaperPropertyRequestPath: "" + readonly property var propertyTranslationApi: ({ + translatePropertyLabelKey: key => pluginApi?.tr(key), + createColor: (r, g, b, a) => Qt.rgba(r, g, b, a) + }) + readonly property var propertyEditorApi: ({ + propertyValueFor: definition => root.propertyValueFor(definition), + numberOr: (value, fallback) => PropertyHelpers.numberOr(value, fallback), + formatSliderValue: (value, step) => PropertyHelpers.formatSliderValue(value, step), + comboChoicesFor: definition => PropertyHelpers.comboChoicesFor(definition), + ensureColorValue: value => PropertyHelpers.ensureColorValue( + value, + (rawValue, type) => PropertyHelpers.parsePropertyValue(rawValue, type, root.propertyTranslationApi.createColor), + root.propertyTranslationApi.createColor + ), + resolvePropertyImageSource: rawValue => PropertyHelpers.resolvePropertyImageSource(rawValue, pendingPath), + serializePropertyValue: (value, type) => PropertyHelpers.serializePropertyValue(value, type), + setPropertyValue: (key, value) => root.setPropertyValue(key, value) + }) readonly property bool extraPropertiesEditorEnabled: cfg.enableExtraPropertiesEditor ?? defaults.enableExtraPropertiesEditor ?? true - readonly property string engineStatusBadgeText: { - if (mainInstance?.checkingEngine ?? false) { - return pluginApi?.tr("panel.statusChecking"); - } - if (!(mainInstance?.engineAvailable ?? false)) { - return pluginApi?.tr("panel.statusUnavailable"); - } - if (mainInstance?.engineRunning ?? false) { - return pluginApi?.tr("panel.statusRunning"); - } - if (mainInstance?.hasAnyConfiguredWallpaper && mainInstance.hasAnyConfiguredWallpaper()) { - return pluginApi?.tr("panel.statusReady"); - } - return pluginApi?.tr("panel.statusStopped"); - } - readonly property color engineStatusBadgeFg: { - if (mainInstance?.checkingEngine ?? false) { - return Color.mSecondary; - } - if (!(mainInstance?.engineAvailable ?? false)) { - return Color.mError; - } - if (mainInstance?.engineRunning ?? false) { - return Color.mPrimary; - } - if (mainInstance?.hasAnyConfiguredWallpaper && mainInstance.hasAnyConfiguredWallpaper()) { - return Color.mTertiary; - } - return Color.mOnSurfaceVariant; - } - readonly property color engineStatusBadgeBg: Qt.alpha(engineStatusBadgeFg, 0.16) readonly property int pageCount: Math.max(1, Math.ceil(visibleWallpapers.length / Math.max(pageSize, 1))) readonly property bool paginationVisible: visibleWallpapers.length > pageSize readonly property int currentPageDisplay: visibleWallpapers.length === 0 ? 0 : currentPage + 1 readonly property int currentPageStartIndex: visibleWallpapers.length === 0 ? 0 : currentPage * pageSize + 1 readonly property int currentPageEndIndex: Math.min((currentPage + 1) * pageSize, visibleWallpapers.length) - readonly property var selectedWallpaperData: { + + function getSelectedWallpaperData() { const target = String(pendingPath || ""); if (target.length === 0) { return null; @@ -139,29 +131,20 @@ Item { } // Basic file and metadata helpers. - function basename(path) { - return WallpaperMetaHelpers.basename(path); - } - - function workshopUrlForWallpaper(item) { - return WallpaperMetaHelpers.workshopUrlForWallpaper(item); + function isVideoMotion(path) { + return WallpaperMetaHelpers.isVideoMotion(path); } - function fileExt(path) { - return WallpaperMetaHelpers.fileExt(path); + function typeLabel(value) { + return WallpaperUiHelpers.typeLabel(value, key => pluginApi?.tr(key)); } - function isVideoMotion(path) { - return WallpaperMetaHelpers.isVideoMotion(path); + function typeBadgeIcon(value) { + return WallpaperUiHelpers.typeBadgeIcon(value); } - function typeLabel(value) { - const key = String(value || "all").toLowerCase(); - if (key === "scene") return pluginApi?.tr("panel.typeScene"); - if (key === "video") return pluginApi?.tr("panel.typeVideo"); - if (key === "web") return pluginApi?.tr("panel.typeWeb"); - if (key === "application") return pluginApi?.tr("panel.typeApplication"); - return pluginApi?.tr("panel.filterAll"); + function dynamicBadgeIcon(isDynamic) { + return WallpaperUiHelpers.dynamicBadgeIcon(isDynamic); } function formatBytes(bytesValue) { @@ -169,10 +152,7 @@ Item { } function sortLabel(value) { - if (value === "date") return pluginApi?.tr("panel.sortDateAdded"); - if (value === "size") return pluginApi?.tr("panel.sortSize"); - if (value === "recent") return pluginApi?.tr("panel.sortRecent"); - return pluginApi?.tr("panel.sortName"); + return WallpaperUiHelpers.sortLabel(value, key => pluginApi?.tr(key)); } // Resolution helpers for badges and filtering. @@ -189,39 +169,7 @@ Item { } function resolutionFilterLabel(value) { - if (value === "8k") return pluginApi?.tr("panel.filterRes8k"); - if (value === "4k") return pluginApi?.tr("panel.filterRes4k"); - if (value === "unknown") return pluginApi?.tr("panel.filterResUnknown"); - return pluginApi?.tr("panel.filterResAll"); - } - - // Extra property parsing and normalization helpers. - function stripHtml(rawText) { - return PropertyHelpers.stripHtml(rawText); - } - - function cleanedPropertyLabel(rawText, fallbackKey) { - return PropertyHelpers.cleanedPropertyLabel(rawText, fallbackKey, key => pluginApi?.tr(key)); - } - - function normalizePropertyLabel(value) { - return PropertyHelpers.normalizePropertyLabel(value, key => pluginApi?.tr(key)); - } - - function isNoisePropertyKey(value) { - return PropertyHelpers.isNoisePropertyKey(value); - } - - function isNoisePropertyLabel(value) { - return PropertyHelpers.isNoisePropertyLabel(value); - } - - function parsePropertyValue(rawValue, type) { - return PropertyHelpers.parsePropertyValue(rawValue, type, (r, g, b, a) => Qt.rgba(r, g, b, a)); - } - - function serializePropertyValue(value, type) { - return PropertyHelpers.serializePropertyValue(value, type); + return WallpaperUiHelpers.resolutionFilterLabel(value, key => pluginApi?.tr(key)); } // Extra property value accessors. @@ -237,26 +185,6 @@ Item { return definition.defaultValue; } - function comboChoicesFor(definition) { - return PropertyHelpers.comboChoicesFor(definition); - } - - function ensureColorValue(value) { - return PropertyHelpers.ensureColorValue( - value, - (rawValue, type) => parsePropertyValue(rawValue, type), - (r, g, b, a) => Qt.rgba(r, g, b, a) - ); - } - - function numberOr(value, fallback) { - return PropertyHelpers.numberOr(value, fallback); - } - - function formatSliderValue(value, step) { - return PropertyHelpers.formatSliderValue(value, step); - } - function setPropertyValue(key, value) { const current = wallpaperPropertyValues || ({}); const next = Object.assign({}, current); @@ -266,134 +194,7 @@ Item { // Property loading and compatibility scan actions. function parseWallpaperPropertiesOutput(rawText) { - const lines = String(rawText || "").split(/\r?\n/); - const definitions = []; - let current = null; - let parsingValues = false; - - function commitCurrent() { - if (!current) { - return; - } - if (["boolean", "slider", "combo", "textinput", "color", "text"].indexOf(current.type) === -1) { - current = null; - parsingValues = false; - return; - } - current.label = cleanedPropertyLabel(current.label, current.key); - if (current.type === "text") { - if (current.label.length === 0 || isNoisePropertyLabel(current.label)) { - current = null; - parsingValues = false; - return; - } - definitions.push({ - key: current.key, - type: "text", - label: current.label, - defaultValue: "" - }); - current = null; - parsingValues = false; - return; - } - if (isNoisePropertyKey(current.key) || isNoisePropertyLabel(current.label)) { - current = null; - parsingValues = false; - return; - } - definitions.push(current); - current = null; - parsingValues = false; - } - - for (const rawLine of lines) { - const line = String(rawLine || ""); - const trimmed = line.trim(); - if (trimmed.length === 0) { - commitCurrent(); - continue; - } - - if (trimmed.indexOf("Unknown object type found:") === 0 - || trimmed.indexOf("ScriptEngine [evaluate]:") === 0 - || trimmed.indexOf("Text objects are not supported yet") === 0 - || trimmed.indexOf("Applying override value for ") === 0) { - continue; - } - - const headerMatch = trimmed.match(/^([^\s].*?)\s+-\s+(slider|boolean|combo|textinput|color|text|scene texture)$/i); - if (headerMatch) { - commitCurrent(); - current = { - key: headerMatch[1].trim(), - type: headerMatch[2].toLowerCase(), - label: undefined, - min: undefined, - max: undefined, - step: undefined, - defaultValue: "", - choices: [] - }; - parsingValues = false; - continue; - } - - if (!current) { - continue; - } - - if (trimmed.indexOf("Text:") === 0) { - current.label = trimmed.substring(5).trim(); - parsingValues = false; - continue; - } - if (trimmed.indexOf("Min:") === 0) { - const parsed = Number(trimmed.substring(4).trim()); - current.min = isNaN(parsed) ? undefined : parsed; - parsingValues = false; - continue; - } - if (trimmed.indexOf("Max:") === 0) { - const parsed = Number(trimmed.substring(4).trim()); - current.max = isNaN(parsed) ? undefined : parsed; - parsingValues = false; - continue; - } - if (trimmed.indexOf("Step:") === 0) { - const parsed = Number(trimmed.substring(5).trim()); - current.step = isNaN(parsed) ? undefined : parsed; - parsingValues = false; - continue; - } - if (trimmed.indexOf("Value:") === 0) { - current.defaultValue = parsePropertyValue(trimmed.substring(6).trim(), current.type); - parsingValues = false; - continue; - } - if (trimmed === "Values:") { - parsingValues = true; - continue; - } - - if (parsingValues && current.type === "combo") { - const valueMatch = trimmed.match(/^(.*?)\s*=\s*(.*)$/); - if (valueMatch) { - const choiceKey = valueMatch[1].trim(); - const choiceName = valueMatch[2].trim(); - current.choices.push({ - key: choiceKey, - name: choiceName, - label: choiceName, - value: choiceKey, - text: choiceName - }); - } - } - } - - commitCurrent(); - return definitions; + return PropertyHelpers.parseWallpaperPropertiesOutput(rawText, root.propertyTranslationApi); } function loadWallpaperProperties(path) { @@ -414,31 +215,88 @@ Item { } function setWallpaperPropertyLoadFailed(path, failed) { + const currentState = propertyCompatibilityStateForPath(path); + if (failed) { + setWallpaperPropertyCompatibilityState(path, "failed"); + return; + } + if (currentState === "failed" || currentState.length === 0) { + setWallpaperPropertyCompatibilityState(path, ""); + } + } + + function setWallpaperPropertyCompatibilityState(path, state) { const wallpaperPath = String(path || "").trim(); if (wallpaperPath.length === 0) { return; } + const normalizedState = String(state || "").trim(); const nextState = Object.assign({}, wallpaperPropertyLoadFailedByPath); - if (failed) { - nextState[wallpaperPath] = true; + if (normalizedState.length > 0) { + nextState[wallpaperPath] = normalizedState; } else { delete nextState[wallpaperPath]; } wallpaperPropertyLoadFailedByPath = nextState; } + function propertyCompatibilityStateForPath(path) { + const key = String(path || "").trim(); + const raw = wallpaperPropertyLoadFailedByPath || ({}); + const value = raw[key]; + if (value === true) { + return "failed"; + } + return String(value || "").trim(); + } + + function propertyCompatibilityBadgeIconForPath(path) { + const state = propertyCompatibilityStateForPath(path); + if (state === "limited") { + return "alert-circle"; + } + if (state === "failed") { + return "alert-triangle"; + } + return ""; + } + + function propertyCompatibilityBadgeTextForPath(path) { + const state = propertyCompatibilityStateForPath(path); + if (state === "limited") { + return pluginApi?.tr("panel.propertiesLimitedBadge"); + } + if (state === "failed") { + return pluginApi?.tr("panel.propertiesFailedBadge"); + } + return ""; + } + + function propertyCompatibilityBadgeColorForPath(path) { + const state = propertyCompatibilityStateForPath(path); + if (state === "limited") { + return Color.mSecondary; + } + if (state === "failed") { + return Color.mError; + } + return Color.mOnSurfaceVariant; + } + + function propertyCompatibilityBadgeBackgroundForPath(path) { + return Qt.alpha(propertyCompatibilityBadgeColorForPath(path), 0.16); + } + function startCompatibilityScan() { const folderPath = String(resolvedWallpapersFolder || "").trim(); if (folderPath.length === 0 || !(mainInstance?.engineAvailable ?? false)) { - pendingCompatibilityScan = false; return; } const pluginDir = pluginApi?.pluginDir || ""; const scriptPath = pluginDir + "/scripts/scan-properties-compatibility.sh"; - pendingCompatibilityScan = false; scanningCompatibility = true; compatibilityScanProcess.command = ["bash", scriptPath, folderPath]; compatibilityScanProcess.running = true; @@ -457,29 +315,40 @@ Item { const parts = line.split("\t"); const path = String(parts[0] || "").trim(); - const failed = String(parts[1] || "0").trim() === "1"; + const statusCode = String(parts[1] || "0").trim(); if (path.length === 0) { continue; } totalCount += 1; - if (failed) { - nextState[path] = true; + if (statusCode === "1") { + nextState[path] = "failed"; + } else if (statusCode === "2") { + nextState[path] = "limited"; } } wallpaperPropertyLoadFailedByPath = nextState; + let limitedCount = 0; + let failedCount = 0; + for (const value of Object.values(nextState)) { + if (value === "limited") { + limitedCount += 1; + } else if (value === "failed") { + failedCount += 1; + } + } return { totalCount: totalCount, - failedCount: Object.keys(nextState).length + failedCount: failedCount, + limitedCount: limitedCount }; } // Dropdown state helpers. function closeDropdowns() { filterDropdownOpen = false; - resolutionDropdownOpen = false; sortDropdownOpen = false; } @@ -487,7 +356,6 @@ Item { filterDropdownX = x; filterDropdownY = y; filterDropdownWidth = width; - resolutionDropdownOpen = false; sortDropdownOpen = false; filterDropdownOpen = true; } @@ -497,28 +365,13 @@ Item { sortDropdownY = y; sortDropdownWidth = width; filterDropdownOpen = false; - resolutionDropdownOpen = false; sortDropdownOpen = true; } - function openResolutionDropdown(x, y, width) { - resolutionDropdownX = x; - resolutionDropdownY = y; - resolutionDropdownWidth = width; - filterDropdownOpen = false; - sortDropdownOpen = false; - resolutionDropdownOpen = true; - } - function applyFilterAction(action) { if (String(action).indexOf("type:") === 0) { selectedType = String(action).substring(5); - } - closeDropdowns(); - } - - function applyResolutionFilterAction(action) { - if (String(action).indexOf("res:") === 0) { + } else if (String(action).indexOf("res:") === 0) { selectedResolution = String(action).substring(4); } closeDropdowns(); @@ -540,25 +393,23 @@ Item { } const remembered = String(pluginApi?.pluginSettings?.panelLastSelectedPath || "").trim(); + root.lastPersistedPath = remembered; if (remembered.length > 0) { pendingPath = remembered; } } function persistPanelMemory(flushToDisk = false) { - if (!pluginApi) { - return; - } - - const current = String(pluginApi?.pluginSettings?.panelLastSelectedPath || ""); const next = String(pendingPath || ""); - if (current === next) { + if (root.lastPersistedPath === next) { return; } - - pluginApi.pluginSettings.panelLastSelectedPath = next; - if (flushToDisk) { - pluginApi.saveSettings(); + root.lastPersistedPath = next; + if (pluginApi) { + pluginApi.pluginSettings.panelLastSelectedPath = next; + if (flushToDisk) { + pluginApi.saveSettings(); + } } } @@ -577,8 +428,6 @@ Item { } function syncSelectionOptionsFromScreen() { - syncGlobalRuntimeOptions(); - const fallbackScreenName = root.singleScreenMode ? (Quickshell.screens[0]?.name || selectedScreenName) : selectedScreenName; if (root.singleScreenMode && selectedScreenName.length === 0 && fallbackScreenName.length > 0) { selectedScreenName = fallbackScreenName; @@ -593,6 +442,11 @@ Item { selectedScaling = String(screenCfg.scaling || defaults.defaultScaling || "fill"); } + function resetSelectionOptionsFromCurrentConfig() { + syncGlobalRuntimeOptions(); + syncSelectionOptionsFromScreen(); + } + // Wallpaper application and list state refresh. function applyPendingSelection() { const path = String(pendingPath || "").trim(); @@ -606,7 +460,8 @@ Item { : (root.singleScreenMode ? (Quickshell.screens[0]?.name || "") : (selectedScreenName || Quickshell.screens[0]?.name || "")); const colorApplyOptions = { "screenName": colorApplyScreen, - "scaling": selectedScaling + "scaling": selectedScaling, + "notify": true }; const options = { "scaling": selectedScaling, "clamp": selectedClamp }; @@ -619,10 +474,10 @@ Item { const customProperties = {}; for (const definition of wallpaperPropertyDefinitions) { const propertyKey = String(definition?.key || ""); - if (propertyKey.length === 0) { + if (propertyKey.length === 0 || !PropertyHelpers.isWritablePropertyType(definition?.type)) { continue; } - customProperties[propertyKey] = serializePropertyValue(propertyValueFor(definition), definition.type); + customProperties[propertyKey] = propertyEditorApi.serializePropertyValue(propertyEditorApi.propertyValueFor(definition), definition.type); } options.customProperties = customProperties; selectedPath = path; @@ -653,53 +508,26 @@ Item { function refreshVisibleWallpapers() { const query = String(searchText || "").trim().toLowerCase(); - let items = wallpaperItems.slice(); - - if (selectedType !== "all") { - items = items.filter(item => String(item.type || "unknown").toLowerCase() === selectedType); - } - - if (selectedResolution !== "all") { - items = items.filter(item => resolutionFilterKey(item.resolution) === selectedResolution); - } - - if (query.length > 0) { - items = items.filter(item => { - return String(item.name || "").toLowerCase().indexOf(query) >= 0 - || String(item.id || "").toLowerCase().indexOf(query) >= 0; - }); - } - - if (sortMode === "date") { - items.sort((a, b) => Number(a.mtime || 0) - Number(b.mtime || 0)); - } else if (sortMode === "size") { - items.sort((a, b) => Number(a.bytes || 0) - Number(b.bytes || 0)); - } else if (sortMode === "recent") { - items.sort((a, b) => Number(b.mtime || 0) - Number(a.mtime || 0)); - } else { - items.sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))); - } - - if (!sortAscending) { - items.reverse(); - } - - visibleWallpapers = items; + visibleWallpapers = WallpaperFilterHelpers.filteredAndSortedWallpapers(wallpaperItems, { + query: query, + selectedType: selectedType, + selectedResolution: selectedResolution, + sortMode: sortMode, + sortAscending: sortAscending, + resolutionFilterKey: resolutionFilterKey + }); Logger.d("LWEController", "Visible wallpapers refreshed", "count=", visibleWallpapers.length, "type=", selectedType, "resolution=", selectedResolution, "sort=", sortMode, "ascending=", sortAscending, "query=", query); } function refreshPagedWallpapers() { - const safePageSize = Math.max(1, Number(pageSize) || 1); - const totalPages = Math.max(1, Math.ceil(visibleWallpapers.length / safePageSize)); - const nextPage = Math.max(0, Math.min(currentPage, totalPages - 1)); + const pageState = WallpaperFilterHelpers.pagedWallpapers(visibleWallpapers, currentPage, pageSize); - if (nextPage !== currentPage) { - currentPage = nextPage; + if (pageState.page !== currentPage) { + currentPage = pageState.page; return; } - const startIndex = nextPage * safePageSize; - pagedWallpapers = visibleWallpapers.slice(startIndex, startIndex + safePageSize); + pagedWallpapers = pageState.items; } function resetPagination() { @@ -765,6 +593,7 @@ Item { return; } pendingPath = path; + sidebarVisible = true; } // Reactive state updates. @@ -776,8 +605,7 @@ Item { onCurrentPageChanged: refreshPagedWallpapers() onPageSizeChanged: refreshPagedWallpapers() onSearchTextChanged: { - refreshVisibleWallpapers(); - resetPagination(); + searchDebounceTimer.restart(); } onSelectedTypeChanged: { refreshVisibleWallpapers(); @@ -795,13 +623,12 @@ Item { refreshVisibleWallpapers(); resetPagination(); } - onSelectedScreenNameChanged: syncSelectionOptionsFromScreen() onPendingPathChanged: { persistPanelMemory(); - loadWallpaperProperties(pendingPath); + propertiesLoadTimer.restart(); } onWallpapersFolderChanged: { - if (!root.pluginApi) { + if (!root.pluginApi || !root.panelInitialized) { return; } mainInstance?.refreshWallpaperCache(true, false); @@ -811,26 +638,24 @@ Item { Logger.i("LWEController", "Panel opened", "screen=", selectedScreenName); rebuildScreenModel(); loadPanelMemory(); - syncSelectionOptionsFromScreen(); + resetSelectionOptionsFromCurrentConfig(); mainInstance?.refreshWallpaperCache(false, false); - loadWallpaperProperties(pendingPath); + panelInitialized = true; + propertiesLoadTimer.restart(); } Component.onDestruction: { persistPanelMemory(true); + if (pluginApi) { + pluginApi.pluginSettings.applyWallpaperColorsOnApply = root.applyWallpaperColorsOnApply; + pluginApi.saveSettings(); + } } // Keep dropdowns aligned with panel width changes. onWidthChanged: { - if (filterDropdownOpen) { - openFilterDropdown(filterDropdownX, filterDropdownY, filterDropdownWidth); - } - if (resolutionDropdownOpen) { - openResolutionDropdown(resolutionDropdownX, resolutionDropdownY, resolutionDropdownWidth); - } - if (sortDropdownOpen) { - openSortDropdown(sortDropdownX, sortDropdownY, sortDropdownWidth); - } + if (filterDropdownOpen) openFilterDropdown(filterDropdownX, filterDropdownY, filterDropdownWidth); + if (sortDropdownOpen) openSortDropdown(sortDropdownX, sortDropdownY, sortDropdownWidth); } // Main instance state hooks. @@ -859,11 +684,7 @@ Item { pluginApi: root.pluginApi mainInstance: root.mainInstance positionTarget: root - engineStatusBadgeText: root.engineStatusBadgeText - engineStatusBadgeFg: root.engineStatusBadgeFg - engineStatusBadgeBg: root.engineStatusBadgeBg scanningCompatibility: root.scanningCompatibility - pendingCompatibilityScan: root.pendingCompatibilityScan searchText: root.searchText selectedType: root.selectedType selectedResolution: root.selectedResolution @@ -872,8 +693,7 @@ Item { typeLabel: root.typeLabel resolutionFilterLabel: root.resolutionFilterLabel sortLabel: root.sortLabel - resolutionButtonWidth: 172 * Style.uiScaleRatio - filterButtonWidth: 172 * Style.uiScaleRatio + filterButtonWidth: 180 * Style.uiScaleRatio sortButtonWidth: 172 * Style.uiScaleRatio onCompatibilityQuickCheckRequested: root.startCompatibilityScan() onReloadRequested: { @@ -893,22 +713,8 @@ Item { pluginApi.togglePanel(screen); } } - onCloseRequested: { - const screen = pluginApi?.panelOpenScreen; - if (pluginApi) { - pluginApi.togglePanel(screen); - } - } - onPendingCompatibilityScanRequested: value => root.pendingCompatibilityScan = value onSearchTextUpdateRequested: text => root.searchText = text onClearSearchRequested: root.searchText = "" - onResolutionDropdownToggleRequested: (x, y, width) => { - if (resolutionDropdownOpen) { - root.closeDropdowns(); - } else { - root.openResolutionDropdown(x, y, width); - } - } onFilterDropdownToggleRequested: (x, y, width) => { if (filterDropdownOpen) { root.closeDropdowns(); @@ -954,10 +760,9 @@ Item { RowLayout { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Style.marginXS spacing: Style.marginM - WallpaperGridSection { + GridSection { pluginApi: root.pluginApi mainInstance: root.mainInstance wallpapers: root.pagedWallpapers @@ -966,7 +771,10 @@ Item { scanningWallpapers: root.scanningWallpapers wallpaperItemsCount: root.wallpaperItems.length visibleWallpaperCount: root.visibleWallpapers.length - propertyLoadFailedByPath: root.wallpaperPropertyLoadFailedByPath + propertyCompatibilityBadgeIconForPath: root.propertyCompatibilityBadgeIconForPath + propertyCompatibilityBadgeTextForPath: root.propertyCompatibilityBadgeTextForPath + propertyCompatibilityBadgeColorForPath: root.propertyCompatibilityBadgeColorForPath + propertyCompatibilityBadgeBackgroundForPath: root.propertyCompatibilityBadgeBackgroundForPath currentPage: root.currentPage pageCount: root.pageCount currentPageDisplay: root.currentPageDisplay @@ -976,17 +784,23 @@ Item { resolutionBadgeIcon: root.resolutionBadgeIcon resolutionBadgeLabel: root.resolutionBadgeLabel typeLabel: root.typeLabel + typeBadgeIcon: root.typeBadgeIcon + dynamicBadgeIcon: root.dynamicBadgeIcon + badgeOrder: root.visibleBadgeOrder isVideoMotion: root.isVideoMotion onWallpaperActivated: path => root.applyPath(path) onPreviousPageRequested: root.goToPreviousPage() onNextPageRequested: root.goToNextPage() } - WallpaperSidebar { + Sidebar { pluginApi: root.pluginApi mainInstance: root.mainInstance - selectedWallpaperData: root.selectedWallpaperData - propertyLoadFailedByPath: root.wallpaperPropertyLoadFailedByPath + selectedWallpaperData: root.getSelectedWallpaperData() + propertyCompatibilityBadgeIconForPath: root.propertyCompatibilityBadgeIconForPath + propertyCompatibilityBadgeTextForPath: root.propertyCompatibilityBadgeTextForPath + propertyCompatibilityBadgeColorForPath: root.propertyCompatibilityBadgeColorForPath + propertyCompatibilityBadgeBackgroundForPath: root.propertyCompatibilityBadgeBackgroundForPath singleScreenMode: root.singleScreenMode applyAllDisplays: root.applyAllDisplays applyTargetExpanded: root.applyTargetExpanded @@ -1008,16 +822,13 @@ Item { resolutionBadgeIcon: root.resolutionBadgeIcon resolutionBadgeLabel: root.resolutionBadgeLabel typeLabel: root.typeLabel + typeBadgeIcon: root.typeBadgeIcon + dynamicBadgeIcon: root.dynamicBadgeIcon + badgeOrder: root.visibleBadgeOrder + showDescription: root.showSidebarDescription isVideoMotion: root.isVideoMotion formatBytes: root.formatBytes - workshopUrlForWallpaper: root.workshopUrlForWallpaper - propertyValueFor: root.propertyValueFor - numberOr: root.numberOr - formatSliderValue: root.formatSliderValue - comboChoicesFor: root.comboChoicesFor - ensureColorValue: root.ensureColorValue - serializePropertyValue: root.serializePropertyValue - setPropertyValue: root.setPropertyValue + propertyEditorApi: root.propertyEditorApi onApplyRequested: root.applyPendingSelection() onApplyAllDisplaysRequested: value => root._applyAllDisplays = value onApplyTargetExpandedRequested: value => root.applyTargetExpanded = value @@ -1031,22 +842,9 @@ Item { onSelectedDisableParallaxRequested: value => root.selectedDisableParallax = value onApplyWallpaperColorsOnApplyRequested: value => { root.applyWallpaperColorsOnApply = value; - if (pluginApi) { - pluginApi.pluginSettings.applyWallpaperColorsOnApply = value; - pluginApi.saveSettings(); - } - } - onWorkshopLinkRequested: workshopUrl => { - if (workshopUrl.length === 0) { - return; - } - - const screen = pluginApi?.panelOpenScreen; - if (pluginApi) { - pluginApi.togglePanel(screen); - } - Qt.openUrlExternally(workshopUrl); } + sidebarVisible: root.sidebarVisible + onSidebarVisibleRequested: value => root.sidebarVisible = value } } @@ -1071,23 +869,17 @@ Item { } } } - } - } PanelDropdowns { pluginApi: root.pluginApi - resolutionDropdownOpen: root.resolutionDropdownOpen filterDropdownOpen: root.filterDropdownOpen sortDropdownOpen: root.sortDropdownOpen selectedResolution: root.selectedResolution selectedType: root.selectedType sortMode: root.sortMode sortAscending: root.sortAscending - resolutionDropdownX: root.resolutionDropdownX - resolutionDropdownY: root.resolutionDropdownY - resolutionDropdownWidth: root.resolutionDropdownWidth filterDropdownX: root.filterDropdownX filterDropdownY: root.filterDropdownY filterDropdownWidth: root.filterDropdownWidth @@ -1095,7 +887,6 @@ Item { sortDropdownY: root.sortDropdownY sortDropdownWidth: root.sortDropdownWidth onCloseRequested: root.closeDropdowns() - onResolutionActionTriggered: action => root.applyResolutionFilterAction(action) onFilterActionTriggered: action => root.applyFilterAction(action) onSortActionTriggered: action => root.applySortAction(action) } @@ -1130,7 +921,7 @@ Item { root.wallpaperPropertyValues = ({}); root.setWallpaperPropertyLoadFailed(requestPath, true); root.wallpaperPropertyError = pluginApi?.tr("panel.propertiesLoadFailed"); - Logger.w("LWEController", "Wallpaper properties load failed", "path=", requestPath, "exitCode=", exitCode, "stderr=", wallpaperPropertyStderr.text); + Logger.w("LWEController", "Wallpaper properties load failed", "path=", requestPath, "exitCode=", exitCode); return; } @@ -1139,7 +930,7 @@ Item { root.wallpaperPropertyDefinitions = definitions; for (const definition of definitions) { if (definition.type === "combo") { - Logger.d("LWEController", "Combo property parsed", "key=", definition.key, "choices=", JSON.stringify(root.comboChoicesFor(definition))); + Logger.d("LWEController", "Combo property parsed", "key=", definition.key, "choices=", JSON.stringify(root.propertyEditorApi.comboChoicesFor(definition))); } } @@ -1148,7 +939,7 @@ Item { for (const definition of definitions) { const propertyKey = String(definition.key || ""); if (savedProperties[propertyKey] !== undefined) { - nextValues[propertyKey] = root.parsePropertyValue(savedProperties[propertyKey], definition.type); + nextValues[propertyKey] = PropertyHelpers.parsePropertyValue(savedProperties[propertyKey], definition.type, root.propertyTranslationApi.createColor); } else { nextValues[propertyKey] = definition.defaultValue; } @@ -1177,11 +968,10 @@ Item { const stderrText = String(compatibilityScanStderr.text || "").trim(); if (exitCode !== 0) { - if (stderrText.length > 0) { - Logger.w("LWEController", "Compatibility scan failed", "exitCode=", exitCode, "stderr=", stderrText); - } else { - Logger.w("LWEController", "Compatibility scan failed", "exitCode=", exitCode); - } + const msg = stderrText.length > 0 + ? "Compatibility scan failed" + ", stderr=" + stderrText + : "Compatibility scan failed"; + Logger.w("LWEController", msg, "exitCode=", exitCode); return; } @@ -1191,11 +981,29 @@ Item { pluginApi?.tr("panel.title"), pluginApi?.tr("panel.compatibilityQuickCheckFinished", { total: result.totalCount, - failed: result.failedCount + failed: result.failedCount, + limited: result.limitedCount }), - result.failedCount > 0 ? "alert-triangle" : "check" + result.failedCount > 0 ? "alert-triangle" : (result.limitedCount > 0 ? "alert-circle" : "check") ); } } + Timer { + id: searchDebounceTimer + interval: 250 + onTriggered: { + refreshVisibleWallpapers(); + resetPagination(); + } + } + + Timer { + id: propertiesLoadTimer + interval: 500 + onTriggered: { + loadWallpaperProperties(pendingPath); + } + } + } diff --git a/linux-wallpaperengine-controller/README.md b/linux-wallpaperengine-controller/README.md index 259eddbb5..1c3f7de29 100644 --- a/linux-wallpaperengine-controller/README.md +++ b/linux-wallpaperengine-controller/README.md @@ -5,9 +5,9 @@ A Noctalia plugin that provides a Wallpaper-Engine wallpaper selector powered by ## Features - Bar widget with quick access to the wallpaper selector panel -- Panel with wallpaper search by name or workshop ID, type filter, resolution filter, sorting, and pagination +- Panel with wallpaper search by name or ID, type filter, resolution filter, sorting, and pagination - Apply wallpapers to all displays or select a specific display target -- Sidebar preview with wallpaper badges for resolution, type, dynamic/static state, and possible compatibility issues, plus a clickable workshop ID +- Sidebar preview with wallpaper badges for resolution, type, dynamic/static state, and possible compatibility issues - Runtime controls for scaling, clamp mode, volume, mute, audio reactive effects, mouse input, and parallax - Optional `Sync wallpaper colors` flow that generates per-display color screenshots and applies Noctalia wallpaper colors for the configured source monitor - Settings resource tools to view and clear color image cache (while preserving cache entries for currently online displays) diff --git a/linux-wallpaperengine-controller/Settings.qml b/linux-wallpaperengine-controller/Settings.qml index df8162fea..c0f3bdb1d 100644 --- a/linux-wallpaperengine-controller/Settings.qml +++ b/linux-wallpaperengine-controller/Settings.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io -import "helpers/ColorCacheHelpers.js" as ColorCacheHelpers +import "helpers/shared/ColorCacheHelpers.js" as ColorCacheHelpers +import "helpers/panel/BadgeHelpers.js" as BadgeHelpers import qs.Commons import qs.Widgets @@ -11,13 +12,16 @@ import qs.Widgets ColumnLayout { id: root + width: parent ? parent.width : 640 * Style.uiScaleRatio + implicitHeight: 720 * Style.uiScaleRatio + Layout.fillWidth: true + property var pluginApi: null readonly property var cfg: pluginApi?.pluginSettings || ({}) readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) property string editWallpapersFolder: cfg.wallpapersFolder ?? defaults.wallpapersFolder ?? "" - property string editAssetsDir: cfg.assetsDir ?? defaults.assetsDir ?? "" property string editIconColor: cfg.iconColor ?? defaults.iconColor ?? "none" property bool editEnableExtraPropertiesEditor: cfg.enableExtraPropertiesEditor ?? defaults.enableExtraPropertiesEditor ?? true property string editDefaultScaling: cfg.defaultScaling ?? defaults.defaultScaling ?? "fill" @@ -32,11 +36,17 @@ ColumnLayout { property bool editDefaultNoFullscreenPause: cfg.defaultNoFullscreenPause ?? defaults.defaultNoFullscreenPause ?? false property bool editDefaultFullscreenPauseOnlyActive: cfg.defaultFullscreenPauseOnlyActive ?? defaults.defaultFullscreenPauseOnlyActive ?? false property bool editAutoApplyOnStartup: cfg.autoApplyOnStartup ?? defaults.autoApplyOnStartup ?? true - property int editWallpaperScanCacheMinutes: cfg.wallpaperScanCacheMinutes ?? defaults.wallpaperScanCacheMinutes ?? 5 + property bool editShowSidebarDescription: cfg.showSidebarDescription ?? defaults.showSidebarDescription ?? false + property int editWallpaperScanCacheMinutes: cfg.wallpaperScanCacheMinutes ?? defaults.wallpaperScanCacheMinutes ?? 10 + readonly property var defaultBadgeOrder: BadgeHelpers.normalizedDefaultOrder(defaults.badgeOrder) + readonly property var defaultBadgeEnabled: BadgeHelpers.normalizedDefaultEnabled(defaults.badgeEnabled) + property var editBadgeOrder: BadgeHelpers.normalizeBadgeOrder(cfg.badgeOrder, defaultBadgeOrder) + property var editBadgeEnabled: BadgeHelpers.normalizeBadgeEnabled(cfg.badgeEnabled, defaultBadgeEnabled) property bool scanning: false property bool refreshingCacheSize: false property bool clearingCache: false property string cacheSizeLabel: pluginApi?.tr("settings.cache.sizeUnknown") + readonly property string pluginCacheDir: ColorCacheHelpers.pluginCacheDir( Settings.cacheDir, pluginApi?.manifest?.id || pluginApi?.pluginId || "linux-wallpaperengine-controller" @@ -44,6 +54,33 @@ ColumnLayout { spacing: Style.marginL + function badgeLabel(key) { + return BadgeHelpers.settingsBadgeLabel(key, badgeKey => pluginApi?.tr(badgeKey)); + } + + function badgeIcon(key) { + return BadgeHelpers.settingsBadgeIcon(key); + } + + function setBadgeEnabled(key, enabled) { + const normalizedKey = String(key || "").trim(); + const next = Object.assign({}, root.editBadgeEnabled || ({})); + next[normalizedKey] = enabled; + root.editBadgeEnabled = next; + } + + function moveBadge(fromIndex, toIndex) { + if (fromIndex < 0 || toIndex < 0 || fromIndex >= editBadgeOrder.length || toIndex >= editBadgeOrder.length || fromIndex === toIndex) { + return; + } + + const next = editBadgeOrder.slice(); + const moved = next[fromIndex]; + next.splice(fromIndex, 1); + next.splice(toIndex, 0, moved); + editBadgeOrder = next; + } + function refreshCacheSize() { if (root.refreshingCacheSize) { return; @@ -71,302 +108,635 @@ ColumnLayout { Component.onCompleted: refreshCacheSize() - NBox { + NTabBar { + id: tabBar Layout.fillWidth: true - implicitHeight: interfaceSection.implicitHeight + Style.marginL * 2 - - ColumnLayout { - id: interfaceSection - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.category.interfaceTitle") - color: Color.mOnSurface - font.weight: Font.Bold - } + NTabButton { + text: pluginApi?.tr("settings.category.interfaceTitle") + tabIndex: 0 + checked: tabBar.currentIndex === 0 + } - NColorChoice { - Layout.fillWidth: true - label: pluginApi?.tr("settings.iconColor.label") - description: pluginApi?.tr("settings.iconColor.description") - currentKey: root.editIconColor - onSelected: key => root.editIconColor = key - } + NTabButton { + text: pluginApi?.tr("settings.category.compatibilityTitle") + tabIndex: 1 + checked: tabBar.currentIndex === 1 + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.enableExtraPropertiesEditor.label") - description: pluginApi?.tr("settings.enableExtraPropertiesEditor.description") - checked: root.editEnableExtraPropertiesEditor - onToggled: checked => root.editEnableExtraPropertiesEditor = checked - } + NTabButton { + text: pluginApi?.tr("settings.defaults.title") + tabIndex: 2 + checked: tabBar.currentIndex === 2 } } - NBox { + NTabView { + id: tabView Layout.fillWidth: true - implicitHeight: resourcesSection.implicitHeight + Style.marginL * 2 + Layout.fillHeight: true + Layout.preferredHeight: 640 * Style.uiScaleRatio + Layout.minimumHeight: 640 * Style.uiScaleRatio + currentIndex: tabBar.currentIndex + + NScrollView { + id: interfaceScroll + Layout.fillWidth: true + Layout.fillHeight: true + contentWidth: availableWidth + showScrollbarWhenScrollable: true + gradientColor: "transparent" + + ColumnLayout { + width: interfaceScroll.availableWidth + Layout.fillWidth: true + spacing: Style.marginL - ColumnLayout { - id: resourcesSection - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM + ColumnLayout { + id: interfaceSection + Layout.fillWidth: true + spacing: Style.marginM + + NColorChoice { + Layout.fillWidth: true + label: pluginApi?.tr("settings.iconColor.label") + description: pluginApi?.tr("settings.iconColor.description") + currentKey: root.editIconColor + onSelected: key => root.editIconColor = key + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.category.compatibilityTitle") - color: Color.mOnSurface - font.weight: Font.Bold - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.enableExtraPropertiesEditor.label") + description: pluginApi?.tr("settings.enableExtraPropertiesEditor.description") + checked: root.editEnableExtraPropertiesEditor + onToggled: checked => root.editEnableExtraPropertiesEditor = checked + } - NTextInput { - Layout.fillWidth: true - label: pluginApi?.tr("settings.wallpapersFolder.label") - description: pluginApi?.tr("settings.wallpapersFolder.description") - placeholderText: pluginApi?.tr("settings.wallpapersFolder.placeholder") - text: root.editWallpapersFolder - onTextChanged: root.editWallpapersFolder = text - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.showSidebarDescription.label") + description: pluginApi?.tr("settings.showSidebarDescription.description") + checked: root.editShowSidebarDescription + onToggled: checked => root.editShowSidebarDescription = checked + } - NButton { - Layout.fillWidth: true - text: pluginApi?.tr("settings.wallpapersFolder.scan") - icon: root.scanning ? "loader" : "search" - enabled: !root.scanning - onClicked: { - root.scanning = true; - scanProcess.running = true; - } - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.badges.title") + color: Color.mOnSurface + font.weight: Font.Bold + } - NTextInput { - Layout.fillWidth: true - label: pluginApi?.tr("settings.assetsDir.label") - description: pluginApi?.tr("settings.assetsDir.description") - placeholderText: pluginApi?.tr("settings.assetsDir.placeholder") - text: root.editAssetsDir - onTextChanged: root.editAssetsDir = text - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.badges.description") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } - NSpinBox { - Layout.fillWidth: true - label: pluginApi?.tr("settings.wallpaperScanCacheMinutes.label") - description: pluginApi?.tr("settings.wallpaperScanCacheMinutes.description") - from: 0 - to: 1440 - stepSize: 1 - value: root.editWallpaperScanCacheMinutes - suffix: pluginApi?.tr("settings.units.minutes") - onValueChanged: if (value !== root.editWallpaperScanCacheMinutes) root.editWallpaperScanCacheMinutes = value + Item { + id: badgeEditorContainer + Layout.fillWidth: true + implicitHeight: badgeEditorColumn.implicitHeight + + property int draggedIndex: -1 + property int dropTargetIndex: -1 + property bool dragStarted: false + property bool potentialDrag: false + property point startPos: Qt.point(0, 0) + readonly property real dragThreshold: 8 + + function cardRect(i) { + const card = badgeCardRepeater.itemAt(i); + if (!card) return Qt.rect(0, 0, 0, 0); + const mapped = card.mapToItem(badgeEditorContainer, 0, 0); + return Qt.rect(mapped.x, mapped.y, card.width, card.height); + } + + function computeDropIndex(mouseY) { + let best = draggedIndex; + let bestDist = Infinity; + const count = root.editBadgeOrder.length; + + for (let i = 0; i < count; ++i) { + if (i === draggedIndex) continue; + const rect = cardRect(i); + if (rect.height <= 0) continue; + const centerY = rect.y + rect.height / 2; + const dist = Math.abs(mouseY - centerY); + if (dist < bestDist) { + bestDist = dist; + best = mouseY < centerY ? i : i + 1; + } + } + + if (best > draggedIndex) best = best - 1; + return Math.max(0, Math.min(count - 1, best)); + } + + function updateDropIndicator() { + const count = root.editBadgeOrder.length; + if (!dragStarted || dropTargetIndex < 0 || count <= 0) return; + const refIndex = Math.min(dropTargetIndex, count - 1); + const refCard = badgeCardRepeater.itemAt(refIndex); + if (!refCard) return; + const mapped = refCard.mapToItem(badgeEditorContainer, 0, 0); + dropIndicator.width = Math.max(0, refCard.width - Style.marginL * 2); + dropIndicator.x = mapped.x + Style.marginL; + dropIndicator.y = dropTargetIndex <= draggedIndex + ? mapped.y - Style.marginXS + : mapped.y + refCard.height - dropIndicator.height + Style.marginXS; + } + + function resetDrag() { + draggedIndex = -1; + dropTargetIndex = -1; + dragStarted = false; + potentialDrag = false; + dragGhost.visible = false; + } + + Rectangle { + id: dropIndicator + width: 0 + height: 3 + radius: Style.radiusXS + color: Color.mPrimary + visible: badgeEditorContainer.dragStarted && badgeEditorContainer.dropTargetIndex !== -1 + z: 10 + } + + Rectangle { + id: dragGhost + width: Math.min(badgeEditorContainer.width, ghostRow.implicitWidth + Style.marginL * 2) + height: ghostRow.implicitHeight + Style.marginM * 2 + radius: Style.radiusM + color: Color.mPrimary + border.width: Style.borderS + border.color: Qt.alpha(Color.mOnPrimary, 0.18) + opacity: 0.9 + visible: false + z: 20 + + RowLayout { + id: ghostRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "grip-vertical" + pointSize: Style.fontSizeS + color: Qt.alpha(Color.mOnPrimary, 0.75) + } + + NIcon { + icon: root.badgeIcon(modelData) + pointSize: Style.fontSizeM + color: Color.mOnPrimary + } + + NText { + id: ghostText + Layout.fillWidth: true + text: "" + color: Color.mOnPrimary + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + + ColumnLayout { + id: badgeEditorColumn + width: parent.width + spacing: Style.marginS + + Repeater { + id: badgeCardRepeater + model: root.editBadgeOrder + + Rectangle { + required property int index + required property var modelData + Layout.fillWidth: true + width: badgeEditorColumn.width + height: badgeCardRow.implicitHeight + Style.marginS * 2 + radius: Style.radiusM + color: Qt.alpha(Color.mSurfaceVariant, 0.6) + border.width: Style.borderS + border.color: badgeDragHandleMouseArea.containsMouse + ? Qt.alpha(Color.mPrimary, 0.55) + : Qt.alpha(Color.mOutline, 0.35) + opacity: badgeEditorContainer.draggedIndex === index && badgeEditorContainer.dragStarted ? 0.3 : 1.0 + + RowLayout { + id: badgeCardRow + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + Item { + Layout.preferredWidth: 24 * Style.uiScaleRatio + Layout.preferredHeight: 34 * Style.uiScaleRatio + + NIcon { + anchors.centerIn: parent + icon: "grip-vertical" + pointSize: Style.fontSizeS + color: badgeDragHandleMouseArea.containsMouse || badgeEditorContainer.draggedIndex === index + ? Color.mPrimary + : Color.mOnSurfaceVariant + } + + MouseArea { + id: badgeDragHandleMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + cursorShape: badgeEditorContainer.dragStarted && badgeEditorContainer.draggedIndex === index + ? Qt.ClosedHandCursor + : Qt.OpenHandCursor + hoverEnabled: true + preventStealing: true + + onPressed: mouse => { + const mapped = badgeDragHandleMouseArea.mapToItem(badgeEditorContainer, mouse.x, mouse.y); + badgeEditorContainer.startPos = Qt.point(mapped.x, mapped.y); + badgeEditorContainer.draggedIndex = index; + badgeEditorContainer.dropTargetIndex = index; + badgeEditorContainer.dragStarted = false; + badgeEditorContainer.potentialDrag = true; + mouse.accepted = true; + } + + onPositionChanged: mouse => { + if (!badgeEditorContainer.potentialDrag || badgeEditorContainer.draggedIndex !== index) return; + const mapped = badgeDragHandleMouseArea.mapToItem(badgeEditorContainer, mouse.x, mouse.y); + const dx = mapped.x - badgeEditorContainer.startPos.x; + const dy = mapped.y - badgeEditorContainer.startPos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (!badgeEditorContainer.dragStarted && dist > badgeEditorContainer.dragThreshold) { + badgeEditorContainer.dragStarted = true; + ghostText.text = root.badgeLabel(modelData); + dragGhost.visible = true; + } + if (badgeEditorContainer.dragStarted) { + dragGhost.x = mapped.x - dragGhost.width / 2; + dragGhost.y = mapped.y - dragGhost.height / 2; + badgeEditorContainer.dropTargetIndex = badgeEditorContainer.computeDropIndex(mapped.y); + badgeEditorContainer.updateDropIndicator(); + } + } + + onReleased: mouse => { + if (badgeEditorContainer.dragStarted) { + const from = badgeEditorContainer.draggedIndex; + const to = badgeEditorContainer.dropTargetIndex; + if (to !== -1 && to !== from) { + root.moveBadge(from, to); + } + } + badgeEditorContainer.resetDrag(); + mouse.accepted = true; + } + + onCanceled: badgeEditorContainer.resetDrag() + } + } + + Rectangle { + Layout.preferredWidth: 34 * Style.uiScaleRatio + Layout.preferredHeight: 34 * Style.uiScaleRatio + radius: Style.radiusS + color: Qt.alpha(Color.mPrimary, 0.12) + + NIcon { + anchors.centerIn: parent + icon: root.badgeIcon(modelData) + pointSize: Style.fontSizeM + color: Color.mPrimary + } + } + + NText { + Layout.fillWidth: true + text: root.badgeLabel(modelData) + color: Color.mOnSurface + font.weight: Font.Medium + elide: Text.ElideRight + } + + NIconButton { + icon: "chevron-up" + tooltipText: pluginApi?.tr("settings.badges.moveUp") + enabled: index > 0 + onClicked: root.moveBadge(index, index - 1) + } + + NIconButton { + icon: "chevron-down" + tooltipText: pluginApi?.tr("settings.badges.moveDown") + enabled: index < root.editBadgeOrder.length - 1 + onClicked: root.moveBadge(index, index + 1) + } + + NToggle { + Layout.alignment: Qt.AlignVCenter + checked: !!root.editBadgeEnabled[String(modelData || "")] + onToggled: checked => root.setBadgeEnabled(modelData, checked) + } + } + } + } + } + } + } } + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.cache.currentSize", { size: root.cacheSizeLabel }) - color: Color.mOnSurfaceVariant - wrapMode: Text.Wrap - } + NScrollView { + id: resourcesScroll + Layout.fillWidth: true + Layout.fillHeight: true + contentWidth: availableWidth + showScrollbarWhenScrollable: true + gradientColor: "transparent" - RowLayout { + ColumnLayout { + width: resourcesScroll.availableWidth Layout.fillWidth: true - spacing: Style.marginS + spacing: Style.marginL + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.category.compatibilityTitle") + color: Color.mOnSurface + font.weight: Font.Bold + font.pointSize: Style.fontSizeL + } - NButton { + NText { Layout.fillWidth: true - text: pluginApi?.tr("settings.cache.refresh") - icon: root.refreshingCacheSize ? "loader" : "refresh" - enabled: !root.refreshingCacheSize && !root.clearingCache - onClicked: root.refreshCacheSize() + text: pluginApi?.tr("settings.resourcesIntro") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap } - NButton { + ColumnLayout { + id: resourcesSection Layout.fillWidth: true - text: pluginApi?.tr("settings.cache.clear") - icon: root.clearingCache ? "loader" : "trash" - enabled: !root.clearingCache && !root.refreshingCacheSize - onClicked: { - root.clearingCache = true; - clearCacheProcess.command = root.clearCacheCommand(); - clearCacheProcess.running = true; + spacing: Style.marginM + + NTextInput { + Layout.fillWidth: true + label: pluginApi?.tr("settings.wallpapersFolder.label") + description: pluginApi?.tr("settings.wallpapersFolder.description") + placeholderText: pluginApi?.tr("settings.wallpapersFolder.placeholder") + text: root.editWallpapersFolder + onTextChanged: root.editWallpapersFolder = text + } + + NButton { + Layout.fillWidth: true + text: pluginApi?.tr("settings.wallpapersFolder.scan") + icon: root.scanning ? "loader" : "search" + enabled: !root.scanning + onClicked: { + root.scanning = true; + scanProcess.running = true; + } + } + + NSpinBox { + Layout.fillWidth: true + label: pluginApi?.tr("settings.wallpaperScanCacheMinutes.label") + description: pluginApi?.tr("settings.wallpaperScanCacheMinutes.description") + from: 0 + to: 1440 + stepSize: 1 + value: root.editWallpaperScanCacheMinutes + suffix: pluginApi?.tr("settings.units.minutes") + onValueChanged: if (value !== root.editWallpaperScanCacheMinutes) root.editWallpaperScanCacheMinutes = value + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.cache.currentSize", { size: root.cacheSizeLabel }) + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NButton { + Layout.fillWidth: true + text: pluginApi?.tr("settings.cache.refresh") + icon: root.refreshingCacheSize ? "loader" : "refresh" + enabled: !root.refreshingCacheSize && !root.clearingCache + onClicked: root.refreshCacheSize() + } + + NButton { + Layout.fillWidth: true + text: pluginApi?.tr("settings.cache.clear") + icon: root.clearingCache ? "loader" : "trash" + enabled: !root.clearingCache && !root.refreshingCacheSize + onClicked: { + root.clearingCache = true; + clearCacheProcess.command = root.clearCacheCommand(); + clearCacheProcess.running = true; + } + } } } } } - } - NBox { - Layout.fillWidth: true - implicitHeight: defaultsSection.implicitHeight + Style.marginL * 2 + NScrollView { + id: defaultsScroll + Layout.fillWidth: true + Layout.fillHeight: true + contentWidth: availableWidth + showScrollbarWhenScrollable: true + gradientColor: "transparent" - ColumnLayout { - id: defaultsSection - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM - - NText { + ColumnLayout { + width: defaultsScroll.availableWidth Layout.fillWidth: true - text: pluginApi?.tr("settings.defaults.title") - color: Color.mOnSurface - font.weight: Font.Bold - } + spacing: Style.marginL - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.defaults.description") - color: Color.mOnSurfaceVariant - wrapMode: Text.Wrap - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.defaults.title") + color: Color.mOnSurface + font.weight: Font.Bold + font.pointSize: Style.fontSizeL + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.category.performanceTitle") - color: Color.mOnSurface - font.weight: Font.Bold - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.defaults.description") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } - NSpinBox { - id: defaultFpsSpinBox - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultFps.label") - description: pluginApi?.tr("settings.defaultFps.description") - from: 1 - to: 240 - stepSize: 1 - value: root.editDefaultFps - suffix: pluginApi?.tr("settings.units.fps") - onValueChanged: if (value !== root.editDefaultFps) root.editDefaultFps = value - } + ColumnLayout { + id: defaultsSection + Layout.fillWidth: true + spacing: Style.marginM - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultNoFullscreenPause.label") - description: pluginApi?.tr("settings.defaultNoFullscreenPause.description") - checked: root.editDefaultNoFullscreenPause - onToggled: checked => root.editDefaultNoFullscreenPause = checked - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.category.performanceTitle") + color: Color.mOnSurface + font.weight: Font.Bold + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultFullscreenPauseOnlyActive.label") - description: pluginApi?.tr("settings.defaultFullscreenPauseOnlyActive.description") - checked: root.editDefaultFullscreenPauseOnlyActive - onToggled: checked => root.editDefaultFullscreenPauseOnlyActive = checked - } + NSpinBox { + id: defaultFpsSpinBox + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultFps.label") + description: pluginApi?.tr("settings.defaultFps.description") + from: 1 + to: 240 + stepSize: 1 + value: root.editDefaultFps + suffix: pluginApi?.tr("settings.units.fps") + onValueChanged: if (value !== root.editDefaultFps) root.editDefaultFps = value + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.autoApplyOnStartup.label") - description: pluginApi?.tr("settings.autoApplyOnStartup.description") - checked: root.editAutoApplyOnStartup - onToggled: checked => root.editAutoApplyOnStartup = checked - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultNoFullscreenPause.label") + description: pluginApi?.tr("settings.defaultNoFullscreenPause.description") + checked: root.editDefaultNoFullscreenPause + onToggled: checked => root.editDefaultNoFullscreenPause = checked + } - NDivider { - Layout.fillWidth: true - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultFullscreenPauseOnlyActive.label") + description: pluginApi?.tr("settings.defaultFullscreenPauseOnlyActive.description") + checked: root.editDefaultFullscreenPauseOnlyActive + onToggled: checked => root.editDefaultFullscreenPauseOnlyActive = checked + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.category.audioTitle") - color: Color.mOnSurface - font.weight: Font.Bold - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.autoApplyOnStartup.label") + description: pluginApi?.tr("settings.autoApplyOnStartup.description") + checked: root.editAutoApplyOnStartup + onToggled: checked => root.editAutoApplyOnStartup = checked + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultMuted.label") - description: pluginApi?.tr("settings.defaultMuted.description") - checked: root.editDefaultMuted - onToggled: checked => root.editDefaultMuted = checked - } + NDivider { + Layout.fillWidth: true + } - NSpinBox { - id: defaultVolumeSpinBox - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultVolume.label") - description: pluginApi?.tr("settings.defaultVolume.description") - from: 0 - to: 100 - stepSize: 1 - suffix: pluginApi?.tr("settings.units.percent") - value: root.editDefaultVolume - enabled: !root.editDefaultMuted - onValueChanged: if (value !== root.editDefaultVolume) root.editDefaultVolume = value - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.category.audioTitle") + color: Color.mOnSurface + font.weight: Font.Bold + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultAudioReactiveEffects.label") - description: pluginApi?.tr("settings.defaultAudioReactiveEffects.description") - checked: root.editDefaultAudioReactiveEffects - onToggled: checked => root.editDefaultAudioReactiveEffects = checked - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultMuted.label") + description: pluginApi?.tr("settings.defaultMuted.description") + checked: root.editDefaultMuted + onToggled: checked => root.editDefaultMuted = checked + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultNoAutomute.label") - description: pluginApi?.tr("settings.defaultNoAutomute.description") - checked: root.editDefaultNoAutomute - onToggled: checked => root.editDefaultNoAutomute = checked - } + NSpinBox { + id: defaultVolumeSpinBox + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultVolume.label") + description: pluginApi?.tr("settings.defaultVolume.description") + from: 0 + to: 100 + stepSize: 1 + suffix: pluginApi?.tr("settings.units.percent") + value: root.editDefaultVolume + enabled: !root.editDefaultMuted + onValueChanged: if (value !== root.editDefaultVolume) root.editDefaultVolume = value + } - NDivider { - Layout.fillWidth: true - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultAudioReactiveEffects.label") + description: pluginApi?.tr("settings.defaultAudioReactiveEffects.description") + checked: root.editDefaultAudioReactiveEffects + onToggled: checked => root.editDefaultAudioReactiveEffects = checked + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("settings.category.displayTitle") - color: Color.mOnSurface - font.weight: Font.Bold - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultNoAutomute.label") + description: pluginApi?.tr("settings.defaultNoAutomute.description") + checked: root.editDefaultNoAutomute + onToggled: checked => root.editDefaultNoAutomute = checked + } - NComboBox { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultScaling.label") - description: pluginApi?.tr("settings.defaultScaling.description") - model: [ - { "key": "fill", "name": pluginApi?.tr("panel.scalingFill") }, - { "key": "fit", "name": pluginApi?.tr("panel.scalingFit") }, - { "key": "stretch", "name": pluginApi?.tr("panel.scalingStretch") }, - { "key": "default", "name": pluginApi?.tr("panel.scalingDefault") } - ] - currentKey: root.editDefaultScaling - onSelected: key => root.editDefaultScaling = key - } + NDivider { + Layout.fillWidth: true + } - NComboBox { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultClamp.label") - description: pluginApi?.tr("settings.defaultClamp.description") - model: [ - { "key": "clamp", "name": pluginApi?.tr("panel.clampClamp") }, - { "key": "border", "name": pluginApi?.tr("panel.clampBorder") }, - { "key": "repeat", "name": pluginApi?.tr("panel.clampRepeat") } - ] - currentKey: root.editDefaultClamp - onSelected: key => root.editDefaultClamp = key - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.category.displayTitle") + color: Color.mOnSurface + font.weight: Font.Bold + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultDisableMouse.label") - description: pluginApi?.tr("settings.defaultDisableMouse.description") - checked: root.editDefaultDisableMouse - onToggled: checked => root.editDefaultDisableMouse = checked - } + NComboBox { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultScaling.label") + description: pluginApi?.tr("settings.defaultScaling.description") + model: [ + { "key": "fill", "name": pluginApi?.tr("panel.scalingFill") }, + { "key": "fit", "name": pluginApi?.tr("panel.scalingFit") }, + { "key": "stretch", "name": pluginApi?.tr("panel.scalingStretch") }, + { "key": "default", "name": pluginApi?.tr("panel.scalingDefault") } + ] + currentKey: root.editDefaultScaling + onSelected: key => root.editDefaultScaling = key + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("settings.defaultDisableParallax.label") - description: pluginApi?.tr("settings.defaultDisableParallax.description") - checked: root.editDefaultDisableParallax - onToggled: checked => root.editDefaultDisableParallax = checked + NComboBox { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultClamp.label") + description: pluginApi?.tr("settings.defaultClamp.description") + model: [ + { "key": "clamp", "name": pluginApi?.tr("panel.clampClamp") }, + { "key": "border", "name": pluginApi?.tr("panel.clampBorder") }, + { "key": "repeat", "name": pluginApi?.tr("panel.clampRepeat") } + ] + currentKey: root.editDefaultClamp + onSelected: key => root.editDefaultClamp = key + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultDisableMouse.label") + description: pluginApi?.tr("settings.defaultDisableMouse.description") + checked: root.editDefaultDisableMouse + onToggled: checked => root.editDefaultDisableMouse = checked + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.defaultDisableParallax.label") + description: pluginApi?.tr("settings.defaultDisableParallax.description") + checked: root.editDefaultDisableParallax + onToggled: checked => root.editDefaultDisableParallax = checked + } + } } } } @@ -382,7 +752,6 @@ ColumnLayout { } pluginApi.pluginSettings.wallpapersFolder = root.editWallpapersFolder; - pluginApi.pluginSettings.assetsDir = root.editAssetsDir; pluginApi.pluginSettings.iconColor = root.editIconColor; pluginApi.pluginSettings.enableExtraPropertiesEditor = root.editEnableExtraPropertiesEditor; pluginApi.pluginSettings.defaultScaling = root.editDefaultScaling; @@ -397,13 +766,14 @@ ColumnLayout { pluginApi.pluginSettings.defaultNoFullscreenPause = root.editDefaultNoFullscreenPause; pluginApi.pluginSettings.defaultFullscreenPauseOnlyActive = root.editDefaultFullscreenPauseOnlyActive; pluginApi.pluginSettings.autoApplyOnStartup = root.editAutoApplyOnStartup; + pluginApi.pluginSettings.showSidebarDescription = root.editShowSidebarDescription; pluginApi.pluginSettings.wallpaperScanCacheMinutes = root.editWallpaperScanCacheMinutes; + pluginApi.pluginSettings.badgeOrder = BadgeHelpers.normalizeBadgeOrder(root.editBadgeOrder, root.defaultBadgeOrder); + pluginApi.pluginSettings.badgeEnabled = BadgeHelpers.normalizeBadgeEnabled(root.editBadgeEnabled, root.defaultBadgeEnabled); pluginApi.saveSettings(); - Logger.d("LWEController", "Settings saved", "wallpapersFolder=", root.editWallpapersFolder, "assetsDir=", root.editAssetsDir, "defaultScaling=", root.editDefaultScaling, "defaultClamp=", root.editDefaultClamp, "defaultFps=", defaultFpsSpinBox.value, "defaultVolume=", defaultVolumeSpinBox.value, "defaultMuted=", root.editDefaultMuted, "defaultAudioReactiveEffects=", root.editDefaultAudioReactiveEffects, "defaultNoAutomute=", root.editDefaultNoAutomute, "defaultDisableMouse=", root.editDefaultDisableMouse, "defaultDisableParallax=", root.editDefaultDisableParallax, "defaultNoFullscreenPause=", root.editDefaultNoFullscreenPause, "defaultFullscreenPauseOnlyActive=", root.editDefaultFullscreenPauseOnlyActive, "autoApplyOnStartup=", root.editAutoApplyOnStartup, "wallpaperScanCacheMinutes=", root.editWallpaperScanCacheMinutes); if (pluginApi.mainInstance) { - Logger.d("LWEController", "Refreshing wallpaper cache and reloading engine after settings save"); pluginApi.mainInstance.refreshWallpaperCache(true, false); if (pluginApi.mainInstance.hasAnyConfiguredWallpaper()) { pluginApi.mainInstance.reload(); @@ -436,6 +806,7 @@ ColumnLayout { Process { id: cacheSizeProcess running: false + command: { const pluginDir = root.pluginApi?.pluginDir || ""; const scriptPath = pluginDir + "/scripts/get-cache-size-bytes.sh"; diff --git a/linux-wallpaperengine-controller/components/WallpaperGridSection.qml b/linux-wallpaperengine-controller/components/WallpaperGridSection.qml deleted file mode 100644 index c67c1da02..000000000 --- a/linux-wallpaperengine-controller/components/WallpaperGridSection.qml +++ /dev/null @@ -1,324 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtMultimedia - -import qs.Commons -import qs.Widgets - -ColumnLayout { - id: root - - property var pluginApi: null - property var mainInstance: null - property var wallpapers: [] - property string pendingPath: "" - property string selectedPath: "" - property bool scanningWallpapers: false - property int wallpaperItemsCount: 0 - property int visibleWallpaperCount: 0 - property var propertyLoadFailedByPath: ({}) - property int currentPage: 0 - property int pageCount: 1 - property int currentPageDisplay: 0 - property int currentPageStartIndex: 0 - property int currentPageEndIndex: 0 - property bool paginationVisible: false - property var resolutionBadgeIcon: null - property var resolutionBadgeLabel: null - property var typeLabel: null - property var isVideoMotion: null - - signal wallpaperActivated(string path) - signal previousPageRequested() - signal nextPageRequested() - - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Style.marginS - - NGridView { - id: gridView - Layout.fillWidth: true - Layout.fillHeight: true - property real minCardWidth: 244 * Style.uiScaleRatio - property real cardGap: Style.marginS - property int columnCount: Math.max(1, Math.floor((availableWidth + cardGap) / (minCardWidth + cardGap))) - cellWidth: (availableWidth - ((columnCount - 1) * cardGap)) / columnCount - cellHeight: 208 * Style.uiScaleRatio - boundsBehavior: Flickable.StopAtBounds - clip: true - - model: root.wallpapers - - delegate: Rectangle { - id: tileCard - required property var modelData - width: gridView.cellWidth - height: gridView.cellHeight - radius: Style.radiusL - color: Qt.alpha(Color.mSurfaceVariant, 0.42) - border.width: root.pendingPath === modelData.path ? 2 : (root.selectedPath === modelData.path ? 1 : 0) - border.color: root.pendingPath === modelData.path ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.35) - clip: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginS - spacing: Style.marginXS - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 136 * Style.uiScaleRatio - radius: Style.radiusM - color: Color.mSurfaceVariant - clip: true - - Image { - anchors.fill: parent - visible: modelData.thumb && modelData.thumb.length > 0 - source: visible ? ("file://" + modelData.thumb) : "" - fillMode: Image.PreserveAspectCrop - cache: false - } - - Loader { - anchors.fill: parent - active: modelData.motionPreview && modelData.motionPreview.length > 0 - sourceComponent: root.isVideoMotion && root.isVideoMotion(modelData.motionPreview) ? motionVideoComponent : motionAnimatedComponent - } - - Component { - id: motionAnimatedComponent - - AnimatedImage { - anchors.fill: parent - source: "file://" + modelData.motionPreview - fillMode: Image.PreserveAspectCrop - cache: false - playing: true - } - } - - Component { - id: motionVideoComponent - - Video { - anchors.fill: parent - autoPlay: true - loops: MediaPlayer.Infinite - muted: true - fillMode: VideoOutput.PreserveAspectCrop - source: "file://" + modelData.motionPreview - } - } - - NIcon { - anchors.centerIn: parent - visible: (!modelData.thumb || modelData.thumb.length === 0) && (!modelData.motionPreview || modelData.motionPreview.length === 0) - icon: "photo" - pointSize: Style.fontSizeXL - color: Color.mOnSurfaceVariant - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginXS - - NText { - Layout.fillWidth: true - text: modelData.name - color: Color.mOnSurface - font.weight: Font.Medium - elide: Text.ElideRight - } - - NIcon { - visible: root.selectedPath === modelData.path - icon: "check" - pointSize: Style.fontSizeL - color: Color.mPrimary - } - } - - Flow { - Layout.fillWidth: true - spacing: Style.marginXS - - Rectangle { - color: Qt.alpha(Color.mSecondary, 0.18) - radius: Style.radiusXS - implicitWidth: typeBadgeText.implicitWidth + Style.marginS * 2 - implicitHeight: typeBadgeText.implicitHeight + Style.marginXS * 2 - - NText { - id: typeBadgeText - anchors.centerIn: parent - text: root.typeLabel ? root.typeLabel(modelData.type) : "" - color: Color.mSecondary - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - - Rectangle { - color: modelData.dynamic ? Qt.alpha(Color.mTertiary, 0.18) : Qt.alpha(Color.mOutline, 0.18) - radius: Style.radiusXS - implicitWidth: motionBadgeText.implicitWidth + Style.marginS * 2 - implicitHeight: motionBadgeText.implicitHeight + Style.marginXS * 2 - - NText { - id: motionBadgeText - anchors.centerIn: parent - text: modelData.dynamic - ? pluginApi?.tr("panel.dynamicBadge") - : pluginApi?.tr("panel.staticBadge") - color: modelData.dynamic ? Color.mTertiary : Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - - Rectangle { - visible: root.resolutionBadgeIcon && root.resolutionBadgeIcon(modelData.resolution).length > 0 - color: Qt.alpha(Color.mSurfaceVariant, 0.24) - radius: Style.radiusXS - implicitWidth: resolutionBadgeRow.implicitWidth + Style.marginS * 2 - implicitHeight: resolutionBadgeRow.implicitHeight + Style.marginXS * 2 - - RowLayout { - id: resolutionBadgeRow - anchors.centerIn: parent - spacing: Style.marginXS - - NIcon { - icon: root.resolutionBadgeIcon ? root.resolutionBadgeIcon(modelData.resolution) : "" - pointSize: Style.fontSizeM - color: Color.mOnSurfaceVariant - } - - NText { - text: root.resolutionBadgeLabel ? root.resolutionBadgeLabel(modelData.resolution) : "" - color: Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - } - - Rectangle { - visible: root.propertyLoadFailedByPath[String(modelData.path || "")] === true - color: Qt.alpha(Color.mError, 0.16) - radius: Style.radiusXS - implicitWidth: propertyFailedBadgeRow.implicitWidth + Style.marginS * 2 - implicitHeight: propertyFailedBadgeRow.implicitHeight + Style.marginXS * 2 - - RowLayout { - id: propertyFailedBadgeRow - anchors.centerIn: parent - spacing: Style.marginXS - - NIcon { - icon: "alert-triangle" - pointSize: Style.fontSizeM - color: Color.mError - } - - NText { - text: pluginApi?.tr("panel.propertiesFailedBadge") - color: Color.mError - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - } - } - } - - MouseArea { - anchors.fill: parent - enabled: root.mainInstance?.engineAvailable ?? false - hoverEnabled: true - onClicked: root.wallpaperActivated(modelData.path) - } - } - - Rectangle { - visible: root.wallpapers.length === 0 && !root.scanningWallpapers - anchors.centerIn: parent - color: "transparent" - width: 300 * Style.uiScaleRatio - height: 140 * Style.uiScaleRatio - - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginS - - NIcon { - Layout.alignment: Qt.AlignHCenter - icon: "photo" - pointSize: Style.fontSizeXL - color: Color.mOnSurfaceVariant - } - - NText { - text: root.wallpaperItemsCount === 0 - ? pluginApi?.tr("panel.emptyAll") - : pluginApi?.tr("panel.emptyFiltered") - color: Color.mOnSurfaceVariant - } - } - } - } - - Rectangle { - Layout.fillWidth: true - visible: root.paginationVisible - implicitHeight: paginationRow.implicitHeight + Style.marginS * 2 - radius: Style.radiusM - color: Qt.alpha(Color.mSurfaceVariant, 0.35) - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.3) - - RowLayout { - id: paginationRow - anchors.fill: parent - anchors.margins: Style.marginS - spacing: Style.marginS - - NButton { - text: pluginApi?.tr("panel.prevPage") - icon: "chevron-left" - enabled: root.currentPage > 0 - onClicked: root.previousPageRequested() - } - - NText { - text: pluginApi?.tr("panel.pageSummary", { - current: root.currentPageDisplay, - total: root.pageCount - }) - color: Color.mOnSurface - font.weight: Font.Medium - } - - NText { - text: pluginApi?.tr("panel.pageRange", { - start: root.currentPageStartIndex, - end: root.currentPageEndIndex, - total: root.visibleWallpaperCount - }) - color: Color.mOnSurfaceVariant - } - - Item { Layout.fillWidth: true } - - NButton { - text: pluginApi?.tr("panel.nextPage") - icon: "chevron-right" - enabled: root.currentPage < root.pageCount - 1 - onClicked: root.nextPageRequested() - } - } - } -} diff --git a/linux-wallpaperengine-controller/components/WallpaperPreviewCard.qml b/linux-wallpaperengine-controller/components/WallpaperPreviewCard.qml deleted file mode 100644 index 2faa16912..000000000 --- a/linux-wallpaperengine-controller/components/WallpaperPreviewCard.qml +++ /dev/null @@ -1,252 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtMultimedia - -import qs.Commons -import qs.Widgets - -ColumnLayout { - id: root - - property var pluginApi: null - property var selectedWallpaperData: null - property var propertyLoadFailedByPath: ({}) - property var resolutionBadgeIcon: null - property var resolutionBadgeLabel: null - property var typeLabel: null - property var isVideoMotion: null - property var formatBytes: null - property var workshopUrlForWallpaper: null - - signal workshopLinkRequested(string url) - - Layout.fillWidth: true - spacing: Style.marginS - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 180 * Style.uiScaleRatio - radius: Style.radiusM - color: Color.mSurfaceVariant - clip: true - - Image { - anchors.fill: parent - visible: root.selectedWallpaperData && (!root.selectedWallpaperData.motionPreview || root.selectedWallpaperData.motionPreview.length === 0) && root.selectedWallpaperData.thumb && root.selectedWallpaperData.thumb.length > 0 - source: visible ? ("file://" + root.selectedWallpaperData.thumb) : "" - fillMode: Image.PreserveAspectCrop - cache: false - } - - AnimatedImage { - anchors.fill: parent - visible: root.selectedWallpaperData && root.selectedWallpaperData.motionPreview && root.selectedWallpaperData.motionPreview.length > 0 && !(root.isVideoMotion && root.isVideoMotion(root.selectedWallpaperData.motionPreview)) - source: visible ? ("file://" + root.selectedWallpaperData.motionPreview) : "" - fillMode: Image.PreserveAspectCrop - cache: false - playing: visible - } - - Video { - anchors.fill: parent - visible: root.selectedWallpaperData && root.selectedWallpaperData.motionPreview && root.selectedWallpaperData.motionPreview.length > 0 && root.isVideoMotion && root.isVideoMotion(root.selectedWallpaperData.motionPreview) - autoPlay: true - loops: MediaPlayer.Infinite - muted: true - fillMode: VideoOutput.PreserveAspectCrop - source: visible ? ("file://" + root.selectedWallpaperData.motionPreview) : "" - } - } - - NText { - Layout.fillWidth: true - text: root.selectedWallpaperData ? root.selectedWallpaperData.name : "" - color: Color.mOnSurface - font.weight: Font.Bold - elide: Text.ElideRight - } - - Flow { - Layout.fillWidth: true - spacing: Style.marginXS - - Rectangle { - visible: root.selectedWallpaperData && root.resolutionBadgeLabel && root.resolutionBadgeLabel(root.selectedWallpaperData.resolution).length > 0 - color: Qt.alpha(Color.mSurfaceVariant, 0.24) - radius: Style.radiusXS - implicitWidth: sidebarResolutionBadgeRow.implicitWidth + Style.marginS * 2 - implicitHeight: sidebarResolutionBadgeRow.implicitHeight + Style.marginXS * 2 - - RowLayout { - id: sidebarResolutionBadgeRow - anchors.centerIn: parent - spacing: Style.marginXS - - NIcon { - icon: root.selectedWallpaperData && root.resolutionBadgeIcon ? root.resolutionBadgeIcon(root.selectedWallpaperData.resolution) : "" - pointSize: Style.fontSizeM - color: Color.mOnSurfaceVariant - } - - NText { - text: root.selectedWallpaperData && root.resolutionBadgeLabel ? root.resolutionBadgeLabel(root.selectedWallpaperData.resolution) : "" - color: Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - } - - Rectangle { - color: Qt.alpha(Color.mSecondary, 0.18) - radius: Style.radiusXS - implicitWidth: sidebarTypeBadgeText.implicitWidth + Style.marginS * 2 - implicitHeight: sidebarTypeBadgeText.implicitHeight + Style.marginXS * 2 - - NText { - id: sidebarTypeBadgeText - anchors.centerIn: parent - text: root.selectedWallpaperData && root.typeLabel ? root.typeLabel(root.selectedWallpaperData.type) : "" - color: Color.mSecondary - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - - Rectangle { - color: root.selectedWallpaperData && root.selectedWallpaperData.dynamic - ? Qt.alpha(Color.mTertiary, 0.18) - : Qt.alpha(Color.mOutline, 0.18) - radius: Style.radiusXS - implicitWidth: sidebarMotionBadgeText.implicitWidth + Style.marginS * 2 - implicitHeight: sidebarMotionBadgeText.implicitHeight + Style.marginXS * 2 - - NText { - id: sidebarMotionBadgeText - anchors.centerIn: parent - text: root.selectedWallpaperData - ? (root.selectedWallpaperData.dynamic - ? pluginApi?.tr("panel.dynamicBadge") - : pluginApi?.tr("panel.staticBadge")) - : "" - color: root.selectedWallpaperData && root.selectedWallpaperData.dynamic ? Color.mTertiary : Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - - Rectangle { - visible: root.propertyLoadFailedByPath[String(root.selectedWallpaperData?.path || "")] === true - color: Qt.alpha(Color.mError, 0.16) - radius: Style.radiusXS - implicitWidth: sidebarPropertyFailedBadgeRow.implicitWidth + Style.marginS * 2 - implicitHeight: sidebarPropertyFailedBadgeRow.implicitHeight + Style.marginXS * 2 - - RowLayout { - id: sidebarPropertyFailedBadgeRow - anchors.centerIn: parent - spacing: Style.marginXS - - NIcon { - icon: "alert-triangle" - pointSize: Style.fontSizeM - color: Color.mError - } - - NText { - text: pluginApi?.tr("panel.propertiesFailedBadge") - color: Color.mError - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - } - } - - GridLayout { - Layout.fillWidth: true - Layout.topMargin: Style.marginM - columns: 2 - columnSpacing: Style.marginM - rowSpacing: Style.marginS - - NText { - text: pluginApi?.tr("panel.infoType") - color: Color.mOnSurfaceVariant - } - - NText { - Layout.fillWidth: true - text: root.selectedWallpaperData && root.typeLabel ? root.typeLabel(root.selectedWallpaperData.type) : "" - color: Color.mOnSurface - horizontalAlignment: Text.AlignRight - wrapMode: Text.Wrap - } - - NText { - text: pluginApi?.tr("panel.infoId") - color: Color.mOnSurfaceVariant - } - - Rectangle { - color: "transparent" - Layout.fillWidth: true - implicitHeight: idValueText.implicitHeight - - NText { - id: idValueText - anchors.left: parent.left - anchors.right: parent.right - text: root.selectedWallpaperData ? root.selectedWallpaperData.id : "" - color: idLinkArea.containsMouse ? Color.mPrimary : Color.mOnSurface - horizontalAlignment: Text.AlignRight - elide: Text.ElideMiddle - } - - MouseArea { - id: idLinkArea - anchors.fill: parent - hoverEnabled: true - enabled: root.workshopUrlForWallpaper && root.workshopUrlForWallpaper(root.selectedWallpaperData).length > 0 - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: { - const workshopUrl = root.workshopUrlForWallpaper ? root.workshopUrlForWallpaper(root.selectedWallpaperData) : ""; - if (workshopUrl.length === 0) { - return; - } - root.workshopLinkRequested(workshopUrl); - } - } - } - - NText { - text: pluginApi?.tr("panel.infoResolution") - color: Color.mOnSurfaceVariant - } - - NText { - Layout.fillWidth: true - text: root.selectedWallpaperData - ? (String(root.selectedWallpaperData.resolution || "unknown") === "unknown" - ? pluginApi?.tr("panel.resolutionUnknown") - : root.selectedWallpaperData.resolution) - : "" - color: Color.mOnSurface - horizontalAlignment: Text.AlignRight - wrapMode: Text.Wrap - } - - NText { - text: pluginApi?.tr("panel.infoSize") - color: Color.mOnSurfaceVariant - } - - NText { - Layout.fillWidth: true - text: root.selectedWallpaperData && root.formatBytes ? root.formatBytes(root.selectedWallpaperData.bytes) : "" - color: Color.mOnSurface - horizontalAlignment: Text.AlignRight - wrapMode: Text.Wrap - } - } -} diff --git a/linux-wallpaperengine-controller/components/WallpaperSidebar.qml b/linux-wallpaperengine-controller/components/WallpaperSidebar.qml deleted file mode 100644 index 5ecacbb98..000000000 --- a/linux-wallpaperengine-controller/components/WallpaperSidebar.qml +++ /dev/null @@ -1,149 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtMultimedia - -import qs.Commons -import qs.Widgets - -import "." - -ColumnLayout { - id: root - - property var pluginApi: null - property var mainInstance: null - property var selectedWallpaperData: null - property var propertyLoadFailedByPath: ({}) - property bool singleScreenMode: true - property bool applyAllDisplays: true - property bool applyTargetExpanded: false - property var screenModel: [] - property string selectedScreenName: "" - property string selectedScaling: "fill" - property string selectedClamp: "clamp" - property int selectedVolume: 100 - property bool selectedMuted: true - property bool selectedAudioReactiveEffects: true - property bool selectedDisableMouse: false - property bool selectedDisableParallax: false - property bool applyWallpaperColorsOnApply: false - property bool applyingWallpaperColors: false - property bool extraPropertiesEditorEnabled: true - property bool loadingWallpaperProperties: false - property string wallpaperPropertyError: "" - property var wallpaperPropertyDefinitions: [] - property var resolutionBadgeIcon: null - property var resolutionBadgeLabel: null - property var typeLabel: null - property var isVideoMotion: null - property var formatBytes: null - property var workshopUrlForWallpaper: null - property var propertyValueFor: null - property var numberOr: null - property var formatSliderValue: null - property var comboChoicesFor: null - property var ensureColorValue: null - property var serializePropertyValue: null - property var setPropertyValue: null - - signal applyRequested() - signal applyAllDisplaysRequested(bool value) - signal applyTargetExpandedRequested(bool value) - signal selectedScreenNameRequested(string value) - signal selectedScalingRequested(string value) - signal selectedClampRequested(string value) - signal selectedVolumeRequested(int value) - signal selectedMutedRequested(bool value) - signal selectedAudioReactiveEffectsRequested(bool value) - signal selectedDisableMouseRequested(bool value) - signal selectedDisableParallaxRequested(bool value) - signal workshopLinkRequested(string url) - signal applyWallpaperColorsOnApplyRequested(bool value) - - Layout.preferredWidth: 340 * Style.uiScaleRatio - Layout.maximumWidth: 340 * Style.uiScaleRatio - Layout.fillWidth: false - Layout.fillHeight: true - visible: root.selectedWallpaperData !== null - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: Style.radiusL - color: Qt.alpha(Color.mSurfaceVariant, 0.35) - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.35) - clip: true - - NScrollView { - id: sidebarScrollView - anchors.fill: parent - anchors.margins: Style.marginM - showScrollbarWhenScrollable: true - gradientColor: "transparent" - - ColumnLayout { - width: sidebarScrollView.availableWidth - spacing: Style.marginS - - WallpaperPreviewCard { - pluginApi: root.pluginApi - selectedWallpaperData: root.selectedWallpaperData - propertyLoadFailedByPath: root.propertyLoadFailedByPath - resolutionBadgeIcon: root.resolutionBadgeIcon - resolutionBadgeLabel: root.resolutionBadgeLabel - typeLabel: root.typeLabel - isVideoMotion: root.isVideoMotion - formatBytes: root.formatBytes - workshopUrlForWallpaper: root.workshopUrlForWallpaper - onWorkshopLinkRequested: workshopUrl => root.workshopLinkRequested(workshopUrl) - } - - WallpaperApplyControls { - pluginApi: root.pluginApi - mainInstance: root.mainInstance - selectedWallpaperData: root.selectedWallpaperData - singleScreenMode: root.singleScreenMode - applyAllDisplays: root.applyAllDisplays - applyTargetExpanded: root.applyTargetExpanded - screenModel: root.screenModel - selectedScreenName: root.selectedScreenName - selectedScaling: root.selectedScaling - selectedClamp: root.selectedClamp - selectedVolume: root.selectedVolume - selectedMuted: root.selectedMuted - selectedAudioReactiveEffects: root.selectedAudioReactiveEffects - selectedDisableMouse: root.selectedDisableMouse - selectedDisableParallax: root.selectedDisableParallax - applyWallpaperColorsOnApply: root.applyWallpaperColorsOnApply - applyingWallpaperColors: root.applyingWallpaperColors - extraPropertiesEditorEnabled: root.extraPropertiesEditorEnabled - loadingWallpaperProperties: root.loadingWallpaperProperties - wallpaperPropertyError: root.wallpaperPropertyError - wallpaperPropertyDefinitions: root.wallpaperPropertyDefinitions - propertyValueFor: root.propertyValueFor - numberOr: root.numberOr - formatSliderValue: root.formatSliderValue - comboChoicesFor: root.comboChoicesFor - ensureColorValue: root.ensureColorValue - serializePropertyValue: root.serializePropertyValue - setPropertyValue: root.setPropertyValue - onApplyRequested: root.applyRequested() - onApplyAllDisplaysRequested: value => root.applyAllDisplaysRequested(value) - onApplyTargetExpandedRequested: value => root.applyTargetExpandedRequested(value) - onSelectedScreenNameRequested: value => root.selectedScreenNameRequested(value) - onSelectedScalingRequested: value => root.selectedScalingRequested(value) - onSelectedClampRequested: value => root.selectedClampRequested(value) - onSelectedVolumeRequested: value => root.selectedVolumeRequested(value) - onSelectedMutedRequested: value => root.selectedMutedRequested(value) - onSelectedAudioReactiveEffectsRequested: value => root.selectedAudioReactiveEffectsRequested(value) - onSelectedDisableMouseRequested: value => root.selectedDisableMouseRequested(value) - onSelectedDisableParallaxRequested: value => root.selectedDisableParallaxRequested(value) - onApplyWallpaperColorsOnApplyRequested: value => root.applyWallpaperColorsOnApplyRequested(value) - } - } - } - } -} diff --git a/linux-wallpaperengine-controller/components/PanelDropdowns.qml b/linux-wallpaperengine-controller/components/panel/PanelDropdowns.qml similarity index 71% rename from linux-wallpaperengine-controller/components/PanelDropdowns.qml rename to linux-wallpaperengine-controller/components/panel/PanelDropdowns.qml index 75a58e2f0..fd07adf8a 100644 --- a/linux-wallpaperengine-controller/components/PanelDropdowns.qml +++ b/linux-wallpaperengine-controller/components/panel/PanelDropdowns.qml @@ -7,16 +7,12 @@ Item { id: root property var pluginApi: null - property bool resolutionDropdownOpen: false property bool filterDropdownOpen: false property bool sortDropdownOpen: false property string selectedResolution: "all" property string selectedType: "all" property string sortMode: "name" property bool sortAscending: true - property real resolutionDropdownX: 0 - property real resolutionDropdownY: 0 - property real resolutionDropdownWidth: 220 * Style.uiScaleRatio property real filterDropdownX: 0 property real filterDropdownY: 0 property real filterDropdownWidth: 220 * Style.uiScaleRatio @@ -25,7 +21,6 @@ Item { property real sortDropdownWidth: 220 * Style.uiScaleRatio signal closeRequested() - signal resolutionActionTriggered(string action) signal filterActionTriggered(string action) signal sortActionTriggered(string action) @@ -33,70 +28,18 @@ Item { MouseArea { anchors.fill: parent - visible: root.filterDropdownOpen || root.resolutionDropdownOpen || root.sortDropdownOpen + visible: root.filterDropdownOpen || root.sortDropdownOpen z: 900 acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: root.closeRequested() } - Rectangle { - visible: root.resolutionDropdownOpen - x: root.resolutionDropdownX - y: root.resolutionDropdownY - width: root.resolutionDropdownWidth - height: Math.min(210 * Style.uiScaleRatio, resolutionList.contentHeight + 2 * Style.marginS) - radius: Style.radiusL - color: Qt.alpha(Color.mSurface, 0.96) - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.45) - z: 901 - - NListView { - id: resolutionList - anchors.fill: parent - anchors.margins: Style.marginS - clip: true - spacing: Style.marginXS - model: [ - { "label": pluginApi?.tr("panel.filterResAll"), "action": "res:all", "selected": root.selectedResolution === "all" }, - { "label": pluginApi?.tr("panel.filterRes4k"), "action": "res:4k", "selected": root.selectedResolution === "4k" }, - { "label": pluginApi?.tr("panel.filterRes8k"), "action": "res:8k", "selected": root.selectedResolution === "8k" }, - { "label": pluginApi?.tr("panel.filterResUnknown"), "action": "res:unknown", "selected": root.selectedResolution === "unknown" } - ] - - delegate: Rectangle { - required property var modelData - width: resolutionList.availableWidth - height: 34 * Style.uiScaleRatio - radius: Style.radiusM - color: modelData.selected ? Qt.alpha(Color.mPrimary, 0.22) : "transparent" - border.width: modelData.selected ? 1 : 0 - border.color: Qt.alpha(Color.mPrimary, 0.45) - - NText { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Style.marginS - text: modelData.label - color: modelData.selected ? Color.mPrimary : Color.mOnSurface - font.weight: modelData.selected ? Font.Medium : Font.Normal - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onClicked: root.resolutionActionTriggered(modelData.action) - } - } - } - } - Rectangle { visible: root.filterDropdownOpen x: root.filterDropdownX y: root.filterDropdownY width: root.filterDropdownWidth - height: Math.min(244 * Style.uiScaleRatio, filterList.contentHeight + 2 * Style.marginS) + height: Math.min(320 * Style.uiScaleRatio, filterList.contentHeight + 2 * Style.marginS) radius: Style.radiusL color: Qt.alpha(Color.mSurface, 0.96) border.width: Style.borderS @@ -114,19 +57,34 @@ Item { { "label": pluginApi?.tr("panel.filterTypeScene"), "action": "type:scene", "selected": root.selectedType === "scene" }, { "label": pluginApi?.tr("panel.filterTypeVideo"), "action": "type:video", "selected": root.selectedType === "video" }, { "label": pluginApi?.tr("panel.filterTypeWeb"), "action": "type:web", "selected": root.selectedType === "web" }, - { "label": pluginApi?.tr("panel.filterTypeApplication"), "action": "type:application", "selected": root.selectedType === "application" } + { "label": pluginApi?.tr("panel.filterTypeApplication"), "action": "type:application", "selected": root.selectedType === "application" }, + { "label": "", "action": "", "selected": false, "separator": true }, + { "label": pluginApi?.tr("panel.filterResAll"), "action": "res:all", "selected": root.selectedResolution === "all" }, + { "label": pluginApi?.tr("panel.filterRes4k"), "action": "res:4k", "selected": root.selectedResolution === "4k" }, + { "label": pluginApi?.tr("panel.filterResUnknown"), "action": "res:unknown", "selected": root.selectedResolution === "unknown" } ] delegate: Rectangle { required property var modelData + readonly property bool isSeparator: !!modelData.separator width: filterList.availableWidth - height: 34 * Style.uiScaleRatio + height: isSeparator ? (8 * Style.uiScaleRatio) : (34 * Style.uiScaleRatio) radius: Style.radiusM - color: modelData.selected ? Qt.alpha(Color.mPrimary, 0.22) : "transparent" - border.width: modelData.selected ? 1 : 0 + color: isSeparator ? "transparent" : (modelData.selected ? Qt.alpha(Color.mPrimary, 0.22) : "transparent") + border.width: isSeparator ? 0 : (modelData.selected ? 1 : 0) border.color: Qt.alpha(Color.mPrimary, 0.45) + Rectangle { + visible: isSeparator + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + height: 0 + color: "transparent" + } + NText { + visible: !isSeparator anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: Style.marginS @@ -136,6 +94,7 @@ Item { } MouseArea { + visible: !isSeparator anchors.fill: parent hoverEnabled: true onClicked: root.filterActionTriggered(modelData.action) diff --git a/linux-wallpaperengine-controller/components/PanelHeader.qml b/linux-wallpaperengine-controller/components/panel/PanelHeader.qml similarity index 50% rename from linux-wallpaperengine-controller/components/PanelHeader.qml rename to linux-wallpaperengine-controller/components/panel/PanelHeader.qml index 893792df7..d36f699bc 100644 --- a/linux-wallpaperengine-controller/components/PanelHeader.qml +++ b/linux-wallpaperengine-controller/components/panel/PanelHeader.qml @@ -10,11 +10,7 @@ Rectangle { property var pluginApi: null property var mainInstance: null property Item positionTarget: null - property string engineStatusBadgeText: "" - property color engineStatusBadgeFg: Color.mOnSurfaceVariant - property color engineStatusBadgeBg: Qt.alpha(engineStatusBadgeFg, 0.16) property bool scanningCompatibility: false - property bool pendingCompatibilityScan: false property string searchText: "" property string selectedType: "all" property string selectedResolution: "all" @@ -23,7 +19,6 @@ Rectangle { property var typeLabel: null property var resolutionFilterLabel: null property var sortLabel: null - property real resolutionButtonWidth: 220 * Style.uiScaleRatio property real filterButtonWidth: 220 * Style.uiScaleRatio property real sortButtonWidth: 220 * Style.uiScaleRatio @@ -31,14 +26,11 @@ Rectangle { signal reloadRequested() signal toggleRunRequested() signal settingsRequested() - signal closeRequested() - signal pendingCompatibilityScanRequested(bool value) signal searchTextUpdateRequested(string text) signal clearSearchRequested() - signal resolutionDropdownToggleRequested(real x, real y, real width) + signal filterDropdownToggleRequested(real x, real y, real width) signal sortDropdownToggleRequested(real x, real y, real width) - function mapButtonGeometry(item) { if (!item || !root.positionTarget) { return { x: 0, y: 0, width: 0 }; @@ -66,124 +58,14 @@ Rectangle { anchors.margins: Style.marginS spacing: Style.marginS - RowLayout { - Layout.fillWidth: true - - NIcon { - icon: "wallpaper-selector" - pointSize: Style.fontSizeL - color: Color.mOnSurface - } - - NText { - text: pluginApi?.tr("panel.title") - font.pointSize: Style.fontSizeL - font.weight: Font.Bold - color: Color.mOnSurface - } - - Rectangle { - radius: Style.radiusXS - color: root.engineStatusBadgeBg - implicitWidth: statusBadgeText.implicitWidth + Style.marginS * 2 - implicitHeight: statusBadgeText.implicitHeight + Style.marginXS * 2 - - NText { - id: statusBadgeText - anchors.centerIn: parent - text: root.engineStatusBadgeText - color: root.engineStatusBadgeFg - font.pointSize: Style.fontSizeXS - font.weight: Font.Medium - } - } - - Item { Layout.fillWidth: true } - - NIconButton { - enabled: (mainInstance?.engineAvailable ?? false) && !root.scanningCompatibility - icon: root.scanningCompatibility ? "loader" : "shield-search" - colorFg: Color.mOnSurface - tooltipText: root.scanningCompatibility - ? pluginApi?.tr("panel.compatibilityQuickCheckRunning") - : pluginApi?.tr("panel.compatibilityQuickCheck") - onClicked: { - if (!root.scanningCompatibility) { - root.pendingCompatibilityScanRequested(true) - } - } - } - - NIconButton { - enabled: !(mainInstance?.scanningWallpapers ?? false) - icon: "refresh" - colorFg: Color.mOnSurface - tooltipText: pluginApi?.tr("panel.refreshWallpapers") - onClicked: root.reloadRequested() - } - - NIconButton { - enabled: mainInstance?.engineAvailable ?? false - icon: mainInstance?.engineRunning ? "player-stop" : "player-play" - colorFg: Color.mOnSurface - tooltipText: mainInstance?.engineRunning ? pluginApi?.tr("panel.stop") : pluginApi?.tr("panel.start") - onClicked: root.toggleRunRequested() - } - - NIconButton { - icon: "settings" - colorFg: Color.mOnSurface - tooltipText: pluginApi?.tr("menu.settings") - onClicked: root.settingsRequested() - } - - NIconButton { - icon: "x" - colorFg: Color.mOnSurface - tooltipText: pluginApi?.tr("panel.closePanel") - onClicked: root.closeRequested() - } - } - - NBox { - visible: root.pendingCompatibilityScan - Layout.fillWidth: true - Layout.preferredHeight: compatibilityConfirmRow.implicitHeight + Style.marginM * 2 - - RowLayout { - id: compatibilityConfirmRow - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM - - NText { - Layout.fillWidth: true - text: pluginApi?.tr("panel.compatibilityQuickCheckConfirm") - pointSize: Style.fontSizeS - color: Color.mOnSurface - wrapMode: Text.WordWrap - } - - NButton { - text: pluginApi?.tr("panel.confirm") - enabled: !root.scanningCompatibility - onClicked: root.compatibilityQuickCheckRequested() - } - - NButton { - text: pluginApi?.tr("panel.cancel") - enabled: !root.scanningCompatibility - onClicked: root.pendingCompatibilityScanRequested(false) - } - } - } - RowLayout { Layout.fillWidth: true Layout.preferredHeight: 48 * Style.uiScaleRatio + spacing: Style.marginXS NTextInput { Layout.fillWidth: true + Layout.minimumWidth: 260 * Style.uiScaleRatio placeholderText: pluginApi?.tr("panel.searchPlaceholder") text: root.searchText onTextChanged: root.searchTextUpdateRequested(text) @@ -198,9 +80,9 @@ Rectangle { } Rectangle { - id: resolutionButton - Layout.preferredWidth: root.resolutionButtonWidth - Layout.maximumWidth: root.resolutionButtonWidth + id: filterButton + Layout.preferredWidth: root.filterButtonWidth + Layout.maximumWidth: root.filterButtonWidth Layout.preferredHeight: 42 * Style.uiScaleRatio radius: Style.radiusL color: Qt.alpha(Color.mSurfaceVariant, 0.42) @@ -214,14 +96,16 @@ Rectangle { spacing: Style.marginXXS NIcon { - icon: "badge-hd" + icon: "adjustments-horizontal" pointSize: Style.fontSizeM color: Color.mOnSurface } NText { Layout.fillWidth: true - text: root.resolutionFilterLabel ? root.resolutionFilterLabel(root.selectedResolution) : "" + text: pluginApi?.tr("panel.filterButtonSummary", { + type: root.typeLabel ? root.typeLabel(root.selectedType) : "" + }) color: Color.mOnSurface elide: Text.ElideRight } @@ -236,16 +120,16 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - const geometry = root.mapButtonGeometry(resolutionButton); - root.resolutionDropdownToggleRequested(geometry.x, geometry.y, geometry.width); + const geometry = root.mapButtonGeometry(filterButton); + root.filterDropdownToggleRequested(geometry.x, geometry.y, geometry.width); } } } Rectangle { - id: filterButton - Layout.preferredWidth: root.filterButtonWidth - Layout.maximumWidth: root.filterButtonWidth + id: sortButton + Layout.preferredWidth: root.sortButtonWidth + Layout.maximumWidth: root.sortButtonWidth Layout.preferredHeight: 42 * Style.uiScaleRatio radius: Style.radiusL color: Qt.alpha(Color.mSurfaceVariant, 0.42) @@ -259,14 +143,17 @@ Rectangle { spacing: Style.marginXXS NIcon { - icon: "adjustments-horizontal" + icon: "arrows-sort" pointSize: Style.fontSizeM color: Color.mOnSurface } NText { Layout.fillWidth: true - text: pluginApi?.tr("panel.filterButtonSummary", { type: root.typeLabel ? root.typeLabel(root.selectedType) : "" }) + text: pluginApi?.tr("panel.sortButtonSummary", { + direction: root.sortAscending ? "↑" : "↓", + sort: root.sortLabel ? root.sortLabel(root.sortMode) : "" + }) color: Color.mOnSurface elide: Text.ElideRight } @@ -281,59 +168,65 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - const geometry = root.mapButtonGeometry(filterButton); - root.filterDropdownToggleRequested(geometry.x, geometry.y, geometry.width); + const geometry = root.mapButtonGeometry(sortButton); + root.sortDropdownToggleRequested(geometry.x, geometry.y, geometry.width); } } } + Item { + Layout.fillWidth: true + Layout.minimumWidth: 0 + } + Rectangle { - id: sortButton - Layout.preferredWidth: root.sortButtonWidth - Layout.maximumWidth: root.sortButtonWidth - Layout.preferredHeight: 42 * Style.uiScaleRatio radius: Style.radiusL - color: Qt.alpha(Color.mSurfaceVariant, 0.42) + color: Qt.alpha(Color.mSurfaceVariant, 0.34) border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.45) + border.color: Qt.alpha(Color.mOutline, 0.35) + implicitWidth: actionButtons.implicitWidth + Style.marginS * 2 + implicitHeight: actionButtons.implicitHeight + Style.marginXS * 2 RowLayout { + id: actionButtons anchors.fill: parent - anchors.leftMargin: Style.marginS - anchors.rightMargin: Style.marginS + anchors.margins: Style.marginXS spacing: Style.marginXXS - NIcon { - icon: "arrows-sort" - pointSize: Style.fontSizeM - color: Color.mOnSurface + NIconButton { + enabled: !(mainInstance?.scanningWallpapers ?? false) + icon: "refresh" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("panel.refreshWallpapers") + onClicked: root.reloadRequested() } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("panel.sortButtonSummary", { - direction: root.sortAscending ? "↑" : "↓", - sort: root.sortLabel ? root.sortLabel(root.sortMode) : "" - }) - color: Color.mOnSurface - elide: Text.ElideRight + NIconButton { + visible: (mainInstance?.engineAvailable ?? false) + enabled: !root.scanningCompatibility + icon: "shield-search" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("panel.compatibilityQuickCheck") + onClicked: root.compatibilityQuickCheckRequested() } - NIcon { - icon: "chevron-down" - pointSize: Style.fontSizeM - color: Color.mOnSurfaceVariant + NIconButton { + enabled: mainInstance?.engineAvailable ?? false + icon: mainInstance?.engineRunning ? "player-stop" : "player-play" + colorFg: Color.mOnSurface + tooltipText: mainInstance?.engineRunning ? pluginApi?.tr("panel.stop") : pluginApi?.tr("panel.start") + onClicked: root.toggleRunRequested() } - } - MouseArea { - anchors.fill: parent - onClicked: { - const geometry = root.mapButtonGeometry(sortButton); - root.sortDropdownToggleRequested(geometry.x, geometry.y, geometry.width); + NIconButton { + icon: "settings" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("menu.settings") + onClicked: root.settingsRequested() } } } } + } } diff --git a/linux-wallpaperengine-controller/components/RuntimeErrorBanner.qml b/linux-wallpaperengine-controller/components/panel/RuntimeErrorBanner.qml similarity index 71% rename from linux-wallpaperengine-controller/components/RuntimeErrorBanner.qml rename to linux-wallpaperengine-controller/components/panel/RuntimeErrorBanner.qml index 8c3002081..f88ac1c16 100644 --- a/linux-wallpaperengine-controller/components/RuntimeErrorBanner.qml +++ b/linux-wallpaperengine-controller/components/panel/RuntimeErrorBanner.qml @@ -1,7 +1,9 @@ import QtQuick import QtQuick.Layouts +import Quickshell import qs.Commons +import qs.Services.UI import qs.Widgets Rectangle { @@ -14,14 +16,23 @@ Rectangle { signal errorDetailsExpandedRequested(bool value) signal dismissRequested() + function copiedErrorText() { + const summary = String(mainInstance?.lastError || "").trim(); + const details = String(mainInstance?.lastErrorDetails || "").trim(); + if (details.length === 0) { + return summary; + } + return summary + "\n\n" + details; + } + visible: !!(mainInstance?.lastError && mainInstance.lastError.length > 0) Layout.fillWidth: true implicitHeight: errorBannerContent.implicitHeight + Style.marginS * 2 Layout.preferredHeight: implicitHeight radius: Style.radiusM - color: Color.mSurface + color: Qt.alpha(Color.mError, 0.08) border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.2) + border.color: Qt.alpha(Color.mError, 0.32) ColumnLayout { id: errorBannerContent @@ -44,12 +55,25 @@ Rectangle { NText { text: pluginApi?.tr("panel.errorBannerTitle") - color: Color.mOnSurface + color: Color.mError font.weight: Font.Bold } Item { Layout.fillWidth: true } + NIconButton { + icon: "copy" + tooltipText: pluginApi?.tr("panel.errorCopy") + onClicked: { + const text = root.copiedErrorText(); + if (text.length === 0) { + return; + } + Quickshell.clipboardText = text; + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("panel.errorCopied"), "copy"); + } + } + NButton { text: root.errorDetailsExpanded ? pluginApi?.tr("panel.errorHideDetails") @@ -70,8 +94,7 @@ Rectangle { text: mainInstance?.lastError ?? "" color: Color.mOnSurface wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight + font.weight: Font.Medium } Rectangle { @@ -79,9 +102,9 @@ Rectangle { Layout.fillWidth: true Layout.preferredHeight: 136 * Style.uiScaleRatio radius: Style.radiusS - color: Qt.alpha(Color.mSurfaceVariant, 0.35) + color: Qt.alpha(Color.mSurface, 0.55) border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.25) + border.color: Qt.alpha(Color.mError, 0.18) NScrollView { anchors.fill: parent diff --git a/linux-wallpaperengine-controller/components/shared/CardGridSection.qml b/linux-wallpaperengine-controller/components/shared/CardGridSection.qml new file mode 100644 index 000000000..ca80742e8 --- /dev/null +++ b/linux-wallpaperengine-controller/components/shared/CardGridSection.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Layouts + +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var pluginApi: null + property var items: [] + property Component cardDelegate: null + property real minCardWidth: 244 * Style.uiScaleRatio + property real cardGap: Style.marginS + property real cellHeight: 208 * Style.uiScaleRatio + property bool showEmptyState: items.length === 0 + property string emptyIcon: "photo" + property string emptyText: "" + property bool paginationVisible: false + property int currentPage: 0 + property int pageCount: 1 + property int currentPageDisplay: 0 + property int currentPageStartIndex: 0 + property int currentPageEndIndex: 0 + property int totalVisibleCount: 0 + + signal previousPageRequested() + signal nextPageRequested() + + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginS + + NGridView { + id: gridView + Layout.fillWidth: true + Layout.fillHeight: true + property int columnCount: Math.max(1, Math.floor((availableWidth + root.cardGap) / (root.minCardWidth + root.cardGap))) + cellWidth: (availableWidth - ((columnCount - 1) * root.cardGap)) / columnCount + cellHeight: root.cellHeight + boundsBehavior: Flickable.StopAtBounds + clip: true + + model: root.items + delegate: root.cardDelegate + + Rectangle { + visible: root.showEmptyState + anchors.centerIn: parent + color: "transparent" + width: 320 * Style.uiScaleRatio + height: 140 * Style.uiScaleRatio + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginS + + NIcon { + Layout.alignment: Qt.AlignHCenter + icon: root.emptyIcon + pointSize: Style.fontSizeXL + color: Color.mOnSurfaceVariant + } + + NText { + text: root.emptyText + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + } + } + } + } + + Rectangle { + Layout.fillWidth: true + visible: root.paginationVisible + implicitHeight: paginationRow.implicitHeight + Style.marginS * 2 + radius: Style.radiusM + color: Qt.alpha(Color.mSurface, 0.78) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.3) + + RowLayout { + id: paginationRow + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + NButton { + text: pluginApi?.tr("panel.prevPage") + icon: "chevron-left" + enabled: root.currentPage > 0 + onClicked: root.previousPageRequested() + } + + NText { + text: pluginApi?.tr("panel.pageSummary", { + current: root.currentPageDisplay, + total: root.pageCount + }) + color: Color.mOnSurface + font.weight: Font.Medium + } + + NText { + text: pluginApi?.tr("panel.pageRange", { + start: root.currentPageStartIndex, + end: root.currentPageEndIndex, + total: root.totalVisibleCount + }) + color: Color.mOnSurfaceVariant + } + + Item { Layout.fillWidth: true } + + NButton { + text: pluginApi?.tr("panel.nextPage") + icon: "chevron-right" + enabled: root.currentPage < root.pageCount - 1 + onClicked: root.nextPageRequested() + } + } + } +} diff --git a/linux-wallpaperengine-controller/components/shared/SidebarPanel.qml b/linux-wallpaperengine-controller/components/shared/SidebarPanel.qml new file mode 100644 index 000000000..1e5b49d81 --- /dev/null +++ b/linux-wallpaperengine-controller/components/shared/SidebarPanel.qml @@ -0,0 +1,71 @@ +import QtQuick +import QtQuick.Layouts + +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + default property alias content: contentColumn.data + property alias footerContent: footerRow.data + + property bool panelVisible: true + property real panelWidth: 340 * Style.uiScaleRatio + + Layout.preferredWidth: panelWidth + Layout.maximumWidth: panelWidth + Layout.fillWidth: false + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + visible: panelVisible + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusL + color: Qt.alpha(Color.mSurfaceVariant, 0.35) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.35) + clip: true + + NScrollView { + id: sidebarScrollView + anchors.fill: parent + anchors.margins: Style.marginM + anchors.bottomMargin: Style.marginM + 56 * Style.uiScaleRatio + contentWidth: availableWidth + showScrollbarWhenScrollable: true + gradientColor: "transparent" + + ColumnLayout { + id: contentColumn + width: sidebarScrollView.availableWidth + spacing: Style.marginS + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: footerRow.implicitHeight > 0 ? footerRow.implicitHeight + Style.marginM * 2 : 0 + color: Qt.rgba(0, 0, 0, 0) + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } + GradientStop { position: 1.0; color: Qt.alpha(Color.mSurface, 0.32) } + } + + RowLayout { + id: footerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Style.marginM + spacing: Style.marginS + } + } + } +} diff --git a/linux-wallpaperengine-controller/components/WallpaperApplyControls.qml b/linux-wallpaperengine-controller/components/wallpaper/ApplyControls.qml similarity index 86% rename from linux-wallpaperengine-controller/components/WallpaperApplyControls.qml rename to linux-wallpaperengine-controller/components/wallpaper/ApplyControls.qml index 5e89242b0..b1dc04e09 100644 --- a/linux-wallpaperengine-controller/components/WallpaperApplyControls.qml +++ b/linux-wallpaperengine-controller/components/wallpaper/ApplyControls.qml @@ -2,20 +2,27 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import "." + import qs.Commons import qs.Widgets ColumnLayout { id: root + // Shared context. property var pluginApi: null property var mainInstance: null property var selectedWallpaperData: null + + // Apply target selection. property bool singleScreenMode: true property bool applyAllDisplays: true property bool applyTargetExpanded: false property var screenModel: [] property string selectedScreenName: "" + + // Per-apply runtime options. property string selectedScaling: "fill" property string selectedClamp: "clamp" property int selectedVolume: 100 @@ -25,18 +32,16 @@ ColumnLayout { property bool selectedDisableParallax: false property bool applyWallpaperColorsOnApply: false property bool applyingWallpaperColors: false + property bool showApplyButton: true + + // Extra wallpaper property editor state. property bool extraPropertiesEditorEnabled: true property bool loadingWallpaperProperties: false property string wallpaperPropertyError: "" property var wallpaperPropertyDefinitions: [] - property var propertyValueFor: null - property var numberOr: null - property var formatSliderValue: null - property var comboChoicesFor: null - property var ensureColorValue: null - property var serializePropertyValue: null - property var setPropertyValue: null + property var propertyEditorApi: null + // Upstream actions. signal applyRequested() signal applyAllDisplaysRequested(bool value) signal applyTargetExpandedRequested(bool value) @@ -50,28 +55,21 @@ ColumnLayout { signal selectedDisableParallaxRequested(bool value) signal applyWallpaperColorsOnApplyRequested(bool value) - readonly property string applyButtonText: { - if (root.singleScreenMode) { - return pluginApi?.tr("panel.confirmApply"); - } - - if (root.applyAllDisplays) { - return pluginApi?.tr("panel.applyAllDisplays"); - } - - return pluginApi?.tr("panel.applySingleDisplay", { screen: root.selectedScreenName }); - } - Layout.fillWidth: true spacing: Style.marginS + // Primary apply action and display target chooser. RowLayout { Layout.fillWidth: true + visible: root.showApplyButton || (!root.singleScreenMode) spacing: Style.marginS NButton { + visible: root.showApplyButton Layout.fillWidth: true - text: root.applyButtonText + text: root.applyAllDisplays + ? pluginApi?.tr("panel.applyAllDisplays") + : pluginApi?.tr("panel.applySingleDisplay", { screen: root.selectedScreenName }) icon: "check" enabled: (root.mainInstance?.engineAvailable ?? false) && !!root.selectedWallpaperData onClicked: root.applyRequested() @@ -80,10 +78,12 @@ ColumnLayout { NIconButton { Layout.preferredWidth: 42 * Style.uiScaleRatio Layout.preferredHeight: 42 * Style.uiScaleRatio - visible: !root.singleScreenMode + visible: root.showApplyButton && !root.singleScreenMode enabled: root.mainInstance?.engineAvailable ?? false - icon: root.applyTargetExpanded ? "chevron-up" : "chevron-down" - tooltipText: pluginApi?.tr("panel.applyTarget") + icon: "device-desktop" + tooltipText: root.applyAllDisplays + ? pluginApi?.tr("panel.targetAllDisplays") + : pluginApi?.tr("panel.targetSingleDisplay", { screen: root.selectedScreenName }) onClicked: root.applyTargetExpandedRequested(!root.applyTargetExpanded) } } @@ -236,6 +236,7 @@ ColumnLayout { onToggled: checked => root.applyWallpaperColorsOnApplyRequested(checked) } + // Optional extra property editor for wallpapers that expose editable values. ColumnLayout { Layout.fillWidth: true visible: root.extraPropertiesEditorEnabled @@ -247,18 +248,12 @@ ColumnLayout { Layout.bottomMargin: Style.marginM } - WallpaperPropertiesEditor { + PropertiesEditor { pluginApi: root.pluginApi loadingWallpaperProperties: root.loadingWallpaperProperties wallpaperPropertyError: root.wallpaperPropertyError wallpaperPropertyDefinitions: root.wallpaperPropertyDefinitions - propertyValueFor: root.propertyValueFor - numberOr: root.numberOr - formatSliderValue: root.formatSliderValue - comboChoicesFor: root.comboChoicesFor - ensureColorValue: root.ensureColorValue - serializePropertyValue: root.serializePropertyValue - setPropertyValue: root.setPropertyValue + propertyEditorApi: root.propertyEditorApi } } } diff --git a/linux-wallpaperengine-controller/components/wallpaper/GridSection.qml b/linux-wallpaperengine-controller/components/wallpaper/GridSection.qml new file mode 100644 index 000000000..910f6b8cc --- /dev/null +++ b/linux-wallpaperengine-controller/components/wallpaper/GridSection.qml @@ -0,0 +1,411 @@ +import QtQuick +import QtQuick.Layouts +import QtMultimedia + +import "../shared" + +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var pluginApi: null + property var mainInstance: null + property var wallpapers: [] + property string pendingPath: "" + property string selectedPath: "" + property bool scanningWallpapers: false + property int wallpaperItemsCount: 0 + property int visibleWallpaperCount: 0 + property int currentPage: 0 + property int pageCount: 1 + property int currentPageDisplay: 0 + property int currentPageStartIndex: 0 + property int currentPageEndIndex: 0 + property bool paginationVisible: false + property var propertyCompatibilityBadgeIconForPath: null + property var propertyCompatibilityBadgeTextForPath: null + property var propertyCompatibilityBadgeColorForPath: null + property var propertyCompatibilityBadgeBackgroundForPath: null + property var resolutionBadgeIcon: null + property var resolutionBadgeLabel: null + property var typeLabel: null + property var typeBadgeIcon: null + property var dynamicBadgeIcon: null + property var badgeOrder: [] + property var isVideoMotion: null + + function showBadge(key, modelData) { + const order = root.badgeOrder || []; + if (order.indexOf(key) < 0) { + return false; + } + + if (key === "type") { + return root.typeLabel && root.typeLabel(modelData.type).length > 0; + } + if (key === "dynamic") { + return true; + } + if (key === "music") { + return !!modelData.hasEmbeddedAudio; + } + if (key === "reactive") { + return !!modelData.hasAudioReactive; + } + if (key === "approved") { + return !!modelData.approved; + } + if (key === "resolution") { + return root.resolutionBadgeIcon && root.resolutionBadgeIcon(modelData.resolution).length > 0; + } + if (key === "compatibility") { + return root.propertyCompatibilityBadgeTextForPath + && root.propertyCompatibilityBadgeTextForPath(String(modelData.path || "")).length > 0; + } + + return false; + } + + signal wallpaperActivated(string path) + signal previousPageRequested() + signal nextPageRequested() + + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginS + + Component { + id: wallpaperCardDelegate + +Rectangle { + id: tileCard + required property var modelData + readonly property var wallpaperData: modelData + readonly property bool isPending: root.pendingPath === modelData.path + readonly property bool isSelected: root.selectedPath === modelData.path + readonly property bool isMotion: modelData.motionPreview && modelData.motionPreview.length > 0 + readonly property bool isVideoMotion: root.isVideoMotion && root.isVideoMotion(modelData.motionPreview) + width: GridView.view.cellWidth + height: GridView.view.cellHeight + radius: Style.radiusL + color: Qt.alpha(Color.mSurface, 0.82) + border.width: isPending ? 2 : (isSelected ? 1 : 0) + border.color: isPending ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.45) + clip: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginXS + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 136 * Style.uiScaleRatio + radius: Style.radiusM + color: Color.mSurfaceVariant + clip: true + + Image { + anchors.fill: parent + visible: modelData.thumb && modelData.thumb.length > 0 + source: visible ? ("file://" + modelData.thumb) : "" + fillMode: Image.PreserveAspectCrop + cache: false + } + + Loader { + anchors.fill: parent + active: tileCard.isMotion + sourceComponent: tileCard.isVideoMotion ? motionVideoComponent : motionAnimatedComponent + } + + Component { + id: motionAnimatedComponent + + AnimatedImage { + anchors.fill: parent + source: "file://" + modelData.motionPreview + fillMode: Image.PreserveAspectCrop + cache: false + playing: true + } + } + + Component { + id: motionVideoComponent + + Video { + anchors.fill: parent + autoPlay: true + loops: MediaPlayer.Infinite + muted: true + fillMode: VideoOutput.PreserveAspectCrop + source: "file://" + modelData.motionPreview + } + } + + NIcon { + anchors.centerIn: parent + visible: (!modelData.thumb || modelData.thumb.length === 0) && (!modelData.motionPreview || modelData.motionPreview.length === 0) + icon: "photo" + pointSize: Style.fontSizeXL + color: Color.mOnSurfaceVariant + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + Layout.fillWidth: true + text: modelData.name + color: Color.mOnSurface + elide: Text.ElideRight + font.weight: Font.Medium + } + + NIcon { + visible: root.selectedPath === modelData.path + icon: "check" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + } + + Item { + id: badgeContainer + Layout.fillWidth: true + Layout.preferredHeight: badgeFlow.implicitHeight + readonly property var visibleBadgeKeys: { + const ordered = root.badgeOrder || []; + const result = []; + for (const key of ordered) { + if (root.showBadge(String(key || ""), modelData)) { + result.push(String(key || "")); + } + } + return result; + } + + Flow { + id: badgeFlow + width: parent.width + spacing: Style.marginXS + + Repeater { + model: badgeContainer.visibleBadgeKeys + + Loader { + required property var modelData + sourceComponent: { + const key = String(modelData || ""); + if (key === "type") return typeBadgeComponent; + if (key === "dynamic") return dynamicBadgeComponent; + if (key === "music") return musicBadgeComponent; + if (key === "reactive") return reactiveBadgeComponent; + if (key === "approved") return approvedBadgeComponent; + if (key === "resolution") return resolutionBadgeComponent; + if (key === "compatibility") return compatibilityBadgeComponent; + return null; + } + } + } + } + + Component { + id: typeBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSecondary, 0.18) + radius: Style.radiusXS + implicitWidth: typeBadgeRow.implicitWidth + Style.marginS * 2 + implicitHeight: typeBadgeText.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: typeBadgeRow + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: root.typeBadgeIcon ? root.typeBadgeIcon(tileCard.wallpaperData.type) : "category" + pointSize: Style.fontSizeM + color: Color.mSecondary + } + + NText { + id: typeBadgeText + text: root.typeLabel ? root.typeLabel(tileCard.wallpaperData.type) : "" + color: Color.mSecondary + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + } + } + } + } + + Component { + id: dynamicBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mTertiary, 0.18) + radius: Style.radiusXS + implicitWidth: motionBadgeIcon.implicitWidth + Style.marginXS * 2 + implicitHeight: motionBadgeIcon.implicitHeight + Style.marginXS * 2 + + NIcon { + id: motionBadgeIcon + anchors.centerIn: parent + icon: root.dynamicBadgeIcon ? root.dynamicBadgeIcon(!!tileCard.wallpaperData.dynamic) : (tileCard.wallpaperData.dynamic ? "player-play" : "player-stop") + pointSize: Style.fontSizeM + color: tileCard.wallpaperData.dynamic ? Color.mTertiary : Color.mOnSurfaceVariant + } + } + } + + Component { + id: musicBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mPrimary, 0.16) + radius: Style.radiusXS + implicitWidth: musicBadgeIcon.implicitWidth + Style.marginXS * 2 + implicitHeight: musicBadgeIcon.implicitHeight + Style.marginXS * 2 + + NIcon { + id: musicBadgeIcon + anchors.centerIn: parent + icon: "volume" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + } + } + + Component { + id: reactiveBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSecondary, 0.16) + radius: Style.radiusXS + implicitWidth: reactiveBadgeIcon.implicitWidth + Style.marginXS * 2 + implicitHeight: reactiveBadgeIcon.implicitHeight + Style.marginXS * 2 + + NIcon { + id: reactiveBadgeIcon + anchors.centerIn: parent + icon: "wave-sine" + pointSize: Style.fontSizeM + color: Color.mSecondary + } + } + } + + Component { + id: approvedBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mPrimary, 0.16) + radius: Style.radiusXS + implicitWidth: approvedBadgeIcon.implicitWidth + Style.marginXS * 2 + implicitHeight: approvedBadgeIcon.implicitHeight + Style.marginXS * 2 + + NIcon { + id: approvedBadgeIcon + anchors.centerIn: parent + icon: "rosette-discount-check" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + } + } + + Component { + id: resolutionBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSurfaceVariant, 0.24) + radius: Style.radiusXS + implicitWidth: resolutionBadgeRow.implicitWidth + Style.marginS * 2 + implicitHeight: resolutionBadgeRow.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: resolutionBadgeRow + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "aspect-ratio" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + text: root.resolutionBadgeLabel ? root.resolutionBadgeLabel(tileCard.wallpaperData.resolution) : "" + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + } + } + } + } + + Component { + id: compatibilityBadgeComponent + + Rectangle { + readonly property string compatibilityPath: String(tileCard.wallpaperData.path || "") + readonly property color compatibilityColor: root.propertyCompatibilityBadgeColorForPath ? root.propertyCompatibilityBadgeColorForPath(compatibilityPath) : Color.mError + color: root.propertyCompatibilityBadgeBackgroundForPath + ? root.propertyCompatibilityBadgeBackgroundForPath(compatibilityPath) + : Qt.alpha(compatibilityColor, 0.16) + radius: Style.radiusXS + implicitWidth: compatibilityBadgeIcon.implicitWidth + Style.marginXS * 2 + implicitHeight: compatibilityBadgeIcon.implicitHeight + Style.marginXS * 2 + + NIcon { + id: compatibilityBadgeIcon + anchors.centerIn: parent + icon: "settings-cog" + pointSize: Style.fontSizeM + color: parent.compatibilityColor + } + } + } + } + } + + MouseArea { + anchors.fill: parent + enabled: root.mainInstance?.engineAvailable ?? false + hoverEnabled: true + onClicked: root.wallpaperActivated(modelData.path) + } + } + } + + CardGridSection { + Layout.fillWidth: true + Layout.fillHeight: true + pluginApi: root.pluginApi + items: root.wallpapers + cardDelegate: wallpaperCardDelegate + cellHeight: 208 * Style.uiScaleRatio + showEmptyState: root.wallpapers.length === 0 && !root.scanningWallpapers + emptyIcon: "photo" + emptyText: root.wallpaperItemsCount === 0 + ? pluginApi?.tr("panel.emptyAll") + : pluginApi?.tr("panel.emptyFiltered") + paginationVisible: root.paginationVisible + currentPage: root.currentPage + pageCount: root.pageCount + currentPageDisplay: root.currentPageDisplay + currentPageStartIndex: root.currentPageStartIndex + currentPageEndIndex: root.currentPageEndIndex + totalVisibleCount: root.visibleWallpaperCount + onPreviousPageRequested: root.previousPageRequested() + onNextPageRequested: root.nextPageRequested() + } +} diff --git a/linux-wallpaperengine-controller/components/wallpaper/PreviewCard.qml b/linux-wallpaperengine-controller/components/wallpaper/PreviewCard.qml new file mode 100644 index 000000000..61a4a9122 --- /dev/null +++ b/linux-wallpaperengine-controller/components/wallpaper/PreviewCard.qml @@ -0,0 +1,567 @@ +import QtQuick +import QtQuick.Layouts +import QtMultimedia + +import "../../helpers/panel/DescriptionHelpers.js" as DescriptionHelpers + +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var pluginApi: null + property var selectedWallpaperData: null + property var propertyCompatibilityBadgeIconForPath: null + property var propertyCompatibilityBadgeTextForPath: null + property var propertyCompatibilityBadgeColorForPath: null + property var propertyCompatibilityBadgeBackgroundForPath: null + property var resolutionBadgeIcon: null + property var resolutionBadgeLabel: null + property var typeLabel: null + property var typeBadgeIcon: null + property var dynamicBadgeIcon: null + property var badgeOrder: [] + property var isVideoMotion: null + property var formatBytes: null + property bool showDescription: true + readonly property var visibleBadgeKeys: { + const ordered = root.badgeOrder || []; + const result = []; + for (const key of ordered) { + if (root.badgeVisible(String(key || ""))) { + result.push(String(key || "")); + } + } + return result; + } + property bool descriptionExpanded: false + property bool descriptionShouldCollapse: false + readonly property int collapsedDescriptionLineCount: 6 + readonly property real collapsedDescriptionHeight: descriptionFontMetrics.height * root.collapsedDescriptionLineCount + readonly property string selectedDescriptionMarkup: { + return DescriptionHelpers.toRichDescription(root.selectedWallpaperData?.description || ""); + } + + function evaluateDescriptionCollapse() { + if (root.selectedDescriptionMarkup.length === 0) { + root.descriptionShouldCollapse = false; + return; + } + + const measuredLinesRaw = Number(descriptionMeasureText.lineCount || 0); + const measuredHeight = Number(descriptionMeasureText.contentHeight || 0); + const lineHeight = Math.max(1, Number(descriptionFontMetrics.height || 1)); + const measuredLines = measuredLinesRaw > 0 ? measuredLinesRaw : Math.ceil(measuredHeight / lineHeight); + const rawDescription = String(root.selectedWallpaperData?.description || ""); + const normalizedRaw = rawDescription + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .replace(/\\n/g, "\n"); + const hardBreakCount = normalizedRaw.split(/\n+/).length; + const longTextHint = normalizedRaw.length > 900; + + if (measuredLinesRaw > 0 || measuredHeight > 0) { + root.descriptionShouldCollapse = measuredLines > root.collapsedDescriptionLineCount; + return; + } + + root.descriptionShouldCollapse = hardBreakCount > root.collapsedDescriptionLineCount + 1 || longTextHint; + } + + function badgeVisible(key) { + if (!root.selectedWallpaperData) { + return false; + } + if (key === "resolution") { + return root.resolutionBadgeLabel && root.resolutionBadgeLabel(root.selectedWallpaperData.resolution).length > 0; + } + if (key === "type") { + return root.typeLabel && root.typeLabel(root.selectedWallpaperData.type).length > 0; + } + if (key === "dynamic") { + return true; + } + if (key === "music") { + return !!root.selectedWallpaperData.hasEmbeddedAudio; + } + if (key === "reactive") { + return !!root.selectedWallpaperData.hasAudioReactive; + } + if (key === "approved") { + return !!root.selectedWallpaperData.approved; + } + if (key === "compatibility") { + const path = String(root.selectedWallpaperData.path || ""); + return root.propertyCompatibilityBadgeTextForPath && root.propertyCompatibilityBadgeTextForPath(path).length > 0; + } + return false; + } + + Layout.fillWidth: true + spacing: Style.marginS + + onSelectedWallpaperDataChanged: { + descriptionExpanded = false; + descriptionCollapseEvalTimer.restart(); + } + onSelectedDescriptionMarkupChanged: descriptionCollapseEvalTimer.restart() + onWidthChanged: descriptionCollapseEvalTimer.restart() + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 180 * Style.uiScaleRatio + radius: Style.radiusM + color: Color.mSurfaceVariant + clip: true + + Image { + anchors.fill: parent + visible: root.selectedWallpaperData && (!root.selectedWallpaperData.motionPreview || root.selectedWallpaperData.motionPreview.length === 0) && root.selectedWallpaperData.thumb && root.selectedWallpaperData.thumb.length > 0 + source: visible ? ("file://" + root.selectedWallpaperData.thumb) : "" + fillMode: Image.PreserveAspectCrop + cache: false + } + + AnimatedImage { + anchors.fill: parent + visible: root.selectedWallpaperData && root.selectedWallpaperData.motionPreview && root.selectedWallpaperData.motionPreview.length > 0 && !(root.isVideoMotion && root.isVideoMotion(root.selectedWallpaperData.motionPreview)) + source: visible ? ("file://" + root.selectedWallpaperData.motionPreview) : "" + fillMode: Image.PreserveAspectCrop + cache: false + playing: visible + } + + Video { + anchors.fill: parent + visible: root.selectedWallpaperData && root.selectedWallpaperData.motionPreview && root.selectedWallpaperData.motionPreview.length > 0 && root.isVideoMotion && root.isVideoMotion(root.selectedWallpaperData.motionPreview) + autoPlay: true + loops: MediaPlayer.Infinite + muted: true + fillMode: VideoOutput.PreserveAspectCrop + source: visible ? ("file://" + root.selectedWallpaperData.motionPreview) : "" + } + } + + NText { + Layout.fillWidth: true + text: root.selectedWallpaperData ? root.selectedWallpaperData.name : "" + color: Color.mOnSurface + font.weight: Font.Bold + elide: Text.ElideRight + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: badgeFlow.implicitHeight + + Flow { + id: badgeFlow + width: parent.width + spacing: Style.marginS + + Repeater { + model: root.visibleBadgeKeys + + Loader { + required property var modelData + sourceComponent: { + const key = String(modelData || ""); + if (key === "resolution") return resolutionBadgeComponent; + if (key === "type") return typeBadgeComponent; + if (key === "dynamic") return motionBadgeComponent; + if (key === "music") return musicBadgeComponent; + if (key === "reactive") return reactiveBadgeComponent; + if (key === "approved") return approvedBadgeComponent; + if (key === "compatibility") return compatibilityBadgeComponent; + return null; + } + } + } + + Component { + id: resolutionBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSurfaceVariant, 0.24) + radius: Style.radiusS + width: resolutionBadgeContent.implicitWidth + Style.marginS * 2 + height: resolutionBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: resolutionBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "aspect-ratio" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + text: root.resolutionBadgeLabel ? root.resolutionBadgeLabel(root.selectedWallpaperData.resolution) : "" + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: typeBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSecondary, 0.1) + radius: Style.radiusS + width: typeBadgeContent.implicitWidth + Style.marginS * 2 + height: typeBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: typeBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: root.typeBadgeIcon ? root.typeBadgeIcon(root.selectedWallpaperData.type) : "category" + pointSize: Style.fontSizeM + color: Color.mSecondary + } + + NText { + text: root.typeLabel ? root.typeLabel(root.selectedWallpaperData.type) : "" + color: Color.mSecondary + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: motionBadgeComponent + + Rectangle { + color: root.selectedWallpaperData.dynamic + ? Qt.alpha(Color.mTertiary, 0.1) + : Qt.alpha(Color.mOutline, 0.1) + radius: Style.radiusS + width: motionBadgeContent.implicitWidth + Style.marginS * 2 + height: motionBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: motionBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: root.dynamicBadgeIcon + ? root.dynamicBadgeIcon(!!root.selectedWallpaperData.dynamic) + : (root.selectedWallpaperData.dynamic ? "player-play" : "player-stop") + pointSize: Style.fontSizeM + color: root.selectedWallpaperData.dynamic ? Color.mTertiary : Color.mOnSurfaceVariant + } + + NText { + text: root.selectedWallpaperData.dynamic + ? pluginApi?.tr("panel.dynamicBadge") + : pluginApi?.tr("panel.staticBadge") + color: root.selectedWallpaperData.dynamic ? Color.mTertiary : Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: musicBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mPrimary, 0.1) + radius: Style.radiusS + width: audioBadgeContent.implicitWidth + Style.marginS * 2 + height: audioBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: audioBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "volume" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: pluginApi?.tr("panel.musicBadge") + color: Color.mPrimary + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: reactiveBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mSecondary, 0.1) + radius: Style.radiusS + width: reactiveBadgeContent.implicitWidth + Style.marginS * 2 + height: reactiveBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: reactiveBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "wave-sine" + pointSize: Style.fontSizeM + color: Color.mSecondary + } + + NText { + text: pluginApi?.tr("panel.reactiveBadge") + color: Color.mSecondary + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: approvedBadgeComponent + + Rectangle { + color: Qt.alpha(Color.mPrimary, 0.1) + radius: Style.radiusS + width: approvedBadgeContent.implicitWidth + Style.marginS * 2 + height: approvedBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: approvedBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "rosette-discount-check" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: pluginApi?.tr("panel.approvedBadge") + color: Color.mPrimary + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + + Component { + id: compatibilityBadgeComponent + + Rectangle { + id: compatibilityBadge + readonly property string compatibilityPath: String(root.selectedWallpaperData?.path || "") + readonly property string compatibilityLabel: root.propertyCompatibilityBadgeTextForPath ? root.propertyCompatibilityBadgeTextForPath(compatibilityPath) : "" + readonly property color compatibilityColor: root.propertyCompatibilityBadgeColorForPath ? root.propertyCompatibilityBadgeColorForPath(compatibilityPath) : Color.mError + readonly property string compatibilityIcon: root.propertyCompatibilityBadgeIconForPath ? root.propertyCompatibilityBadgeIconForPath(compatibilityPath) : "alert-triangle" + color: root.propertyCompatibilityBadgeBackgroundForPath + ? root.propertyCompatibilityBadgeBackgroundForPath(compatibilityPath) + : Qt.alpha(compatibilityColor, 0.1) + radius: Style.radiusS + width: compatBadgeContent.implicitWidth + Style.marginS * 2 + height: compatBadgeContent.implicitHeight + Style.marginXS * 2 + + RowLayout { + id: compatBadgeContent + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "settings-cog" + pointSize: Style.fontSizeM + color: compatibilityBadge.compatibilityColor + } + + NText { + text: compatibilityBadge.compatibilityLabel + color: compatibilityBadge.compatibilityColor + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + elide: Text.ElideRight + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + + NText { + text: pluginApi?.tr("panel.infoType") + color: Color.mOnSurfaceVariant + } + + Item { Layout.fillWidth: true } + + NText { + text: root.selectedWallpaperData && root.typeLabel ? root.typeLabel(root.selectedWallpaperData.type) : "" + color: Color.mOnSurface + } + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: pluginApi?.tr("panel.infoId") + color: Color.mOnSurfaceVariant + } + + Item { Layout.fillWidth: true } + + NText { + text: root.selectedWallpaperData ? root.selectedWallpaperData.id : "" + color: Color.mOnSurface + elide: Text.ElideMiddle + } + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: pluginApi?.tr("panel.infoResolution") + color: Color.mOnSurfaceVariant + } + + Item { Layout.fillWidth: true } + + NText { + text: root.selectedWallpaperData + ? (String(root.selectedWallpaperData.resolution || "unknown") === "unknown" + ? pluginApi?.tr("panel.resolutionUnknown") + : root.selectedWallpaperData.resolution) + : "" + color: Color.mOnSurface + } + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: pluginApi?.tr("panel.infoSize") + color: Color.mOnSurfaceVariant + } + + Item { Layout.fillWidth: true } + + NText { + text: root.selectedWallpaperData && root.formatBytes ? root.formatBytes(root.selectedWallpaperData.bytes) : "" + color: Color.mOnSurface + } + } + + ColumnLayout { + Layout.fillWidth: true + visible: root.showDescription && root.selectedDescriptionMarkup.length > 0 + spacing: Style.marginXS + + FontMetrics { + id: descriptionFontMetrics + font: descriptionText.font + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("panel.infoDescription") + color: Color.mOnSurfaceVariant + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: { + if (!root.descriptionShouldCollapse || root.descriptionExpanded) { + return descriptionText.implicitHeight + Style.marginS * 2; + } + return root.collapsedDescriptionHeight + Style.marginS * 2; + } + radius: Style.radiusM + color: Qt.alpha(Color.mSurfaceVariant, 0.2) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.22) + clip: true + + NText { + id: descriptionMeasureText + visible: false + width: descriptionText.width + text: root.selectedDescriptionMarkup + textFormat: Text.RichText + wrapMode: Text.Wrap + font: descriptionText.font + onContentHeightChanged: descriptionCollapseEvalTimer.restart() + } + + NText { + id: descriptionText + anchors.fill: parent + anchors.margins: Style.marginS + text: root.selectedDescriptionMarkup + textFormat: Text.RichText + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + linkColor: Color.mPrimary + onLinkActivated: url => Qt.openUrlExternally(url) + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 36 * Style.uiScaleRatio + visible: root.descriptionShouldCollapse && !root.descriptionExpanded + color: Qt.rgba( + Color.mSurfaceVariant.r, + Color.mSurfaceVariant.g, + Color.mSurfaceVariant.b, + 0.28 + ) + } + } + + NButton { + Layout.fillWidth: true + visible: root.descriptionShouldCollapse || root.descriptionExpanded + text: root.descriptionExpanded ? pluginApi?.tr("panel.showLess") : pluginApi?.tr("panel.showMore") + icon: root.descriptionExpanded ? "chevron-up" : "chevron-down" + onClicked: root.descriptionExpanded = !root.descriptionExpanded + } + } + + Timer { + id: descriptionCollapseEvalTimer + interval: 40 + repeat: false + onTriggered: root.evaluateDescriptionCollapse() + } +} diff --git a/linux-wallpaperengine-controller/components/WallpaperPropertiesEditor.qml b/linux-wallpaperengine-controller/components/wallpaper/PropertiesEditor.qml similarity index 53% rename from linux-wallpaperengine-controller/components/WallpaperPropertiesEditor.qml rename to linux-wallpaperengine-controller/components/wallpaper/PropertiesEditor.qml index e7a6d9635..535d1e430 100644 --- a/linux-wallpaperengine-controller/components/WallpaperPropertiesEditor.qml +++ b/linux-wallpaperengine-controller/components/wallpaper/PropertiesEditor.qml @@ -11,13 +11,7 @@ ColumnLayout { property bool loadingWallpaperProperties: false property string wallpaperPropertyError: "" property var wallpaperPropertyDefinitions: [] - property var propertyValueFor: null - property var numberOr: null - property var formatSliderValue: null - property var comboChoicesFor: null - property var ensureColorValue: null - property var serializePropertyValue: null - property var setPropertyValue: null + property var propertyEditorApi: null Layout.fillWidth: true spacing: Style.marginS @@ -70,15 +64,18 @@ ColumnLayout { Layout.fillWidth: true spacing: Style.marginXS - property bool boolValue: !!(root.propertyValueFor ? root.propertyValueFor(modelData) : false) - property real sliderValue: root.numberOr ? root.numberOr(root.propertyValueFor ? root.propertyValueFor(modelData) : 0, 0) : 0 - property string comboValue: String(root.propertyValueFor ? root.propertyValueFor(modelData) : "") - property string textValue: String(root.propertyValueFor ? root.propertyValueFor(modelData) : "") + property bool boolValue: !!(root.propertyEditorApi?.propertyValueFor ? root.propertyEditorApi.propertyValueFor(modelData) : false) + property real sliderValue: root.propertyEditorApi?.numberOr ? root.propertyEditorApi.numberOr(root.propertyEditorApi?.propertyValueFor ? root.propertyEditorApi.propertyValueFor(modelData) : 0, 0) : 0 + property string comboValue: String(root.propertyEditorApi?.propertyValueFor ? root.propertyEditorApi.propertyValueFor(modelData) : "") + property string textValue: String(root.propertyEditorApi?.propertyValueFor ? root.propertyEditorApi.propertyValueFor(modelData) : "") + property string imageValue: modelData.type === "image" + ? String(modelData.imageSource || "") + : "" property color colorValue: Qt.rgba(1, 1, 1, 1) Component.onCompleted: { - if (modelData.type === "color" && root.ensureColorValue && root.propertyValueFor) { - propertyEditor.colorValue = root.ensureColorValue(root.propertyValueFor(modelData)); + if (modelData.type === "color" && root.propertyEditorApi?.ensureColorValue && root.propertyEditorApi?.propertyValueFor) { + propertyEditor.colorValue = root.propertyEditorApi.ensureColorValue(root.propertyEditorApi.propertyValueFor(modelData)); } } @@ -93,8 +90,8 @@ ColumnLayout { return; } propertyEditor.boolValue = checked; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, checked); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, checked); } } } @@ -104,18 +101,18 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 visible: modelData.type === "slider" label: modelData.label - from: root.numberOr ? root.numberOr(modelData.min, 0) : 0 - to: root.numberOr ? root.numberOr(modelData.max, 100) : 100 - stepSize: Math.max(root.numberOr ? root.numberOr(modelData.step, 1) : 1, 0.001) + from: root.propertyEditorApi?.numberOr ? root.propertyEditorApi.numberOr(modelData.min, 0) : 0 + to: root.propertyEditorApi?.numberOr ? root.propertyEditorApi.numberOr(modelData.max, 100) : 100 + stepSize: Math.max(root.propertyEditorApi?.numberOr ? root.propertyEditorApi.numberOr(modelData.step, 1) : 1, 0.001) value: propertyEditor.sliderValue - text: root.formatSliderValue ? root.formatSliderValue(propertyEditor.sliderValue, modelData.step) : String(propertyEditor.sliderValue) + text: root.propertyEditorApi?.formatSliderValue ? root.propertyEditorApi.formatSliderValue(propertyEditor.sliderValue, modelData.step) : String(propertyEditor.sliderValue) onMoved: value => { if (value === propertyEditor.sliderValue) { return; } propertyEditor.sliderValue = value; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, value); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, value); } } } @@ -125,7 +122,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 visible: modelData.type === "combo" label: modelData.label - model: root.comboChoicesFor ? root.comboChoicesFor(modelData) : [] + model: root.propertyEditorApi?.comboChoicesFor ? root.propertyEditorApi.comboChoicesFor(modelData) : [] currentKey: propertyEditor.comboValue onSelected: key => { const normalizedKey = String(key); @@ -133,8 +130,8 @@ ColumnLayout { return; } propertyEditor.comboValue = normalizedKey; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, normalizedKey); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, normalizedKey); } } } @@ -151,8 +148,8 @@ ColumnLayout { return; } propertyEditor.textValue = nextText; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, nextText); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, nextText); } } onAccepted: { @@ -161,8 +158,8 @@ ColumnLayout { return; } propertyEditor.textValue = nextText; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, nextText); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, nextText); } } } @@ -180,6 +177,63 @@ ColumnLayout { bottomPadding: Style.marginXS } + ColumnLayout { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "image" + spacing: Style.marginXS + + NText { + Layout.fillWidth: true + visible: String(modelData.label || "").trim().length > 0 + text: modelData.label + color: Color.mOnSurface + font.pointSize: Style.fontSizeM + wrapMode: Text.Wrap + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 160 * Style.uiScaleRatio + radius: Style.radiusM + color: Qt.alpha(Color.mSurfaceVariant, 0.35) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.35) + clip: true + + Image { + id: sceneTexturePreview + anchors.fill: parent + anchors.margins: Style.marginXS + source: parent.parent.visible && root.propertyEditorApi?.resolvePropertyImageSource + ? root.propertyEditorApi.resolvePropertyImageSource(propertyEditor.imageValue) + : "" + fillMode: Image.PreserveAspectFit + asynchronous: true + cache: false + } + + NText { + anchors.centerIn: parent + visible: sceneTexturePreview.status === Image.Error || sceneTexturePreview.source.length === 0 + text: propertyEditor.imageValue + color: Color.mOnSurfaceVariant + wrapMode: Text.WrapAnywhere + width: parent.width - Style.marginM * 2 + horizontalAlignment: Text.AlignHCenter + } + } + + NText { + Layout.fillWidth: true + visible: sceneTexturePreview.status === Image.Error || sceneTexturePreview.source.length === 0 + text: propertyEditor.imageValue + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeS + wrapMode: Text.WrapAnywhere + } + } + ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: visible ? implicitHeight : 0 @@ -210,15 +264,15 @@ ColumnLayout { selectedColor: propertyEditor.colorValue onColorSelected: color => { propertyEditor.colorValue = color; - if (root.setPropertyValue) { - root.setPropertyValue(modelData.key, color); + if (root.propertyEditorApi?.setPropertyValue) { + root.propertyEditorApi.setPropertyValue(modelData.key, color); } } } NText { Layout.fillWidth: true - text: root.serializePropertyValue ? root.serializePropertyValue(propertyEditor.colorValue, "color") : "" + text: root.propertyEditorApi?.serializePropertyValue ? root.propertyEditorApi.serializePropertyValue(propertyEditor.colorValue, "color") : "" color: Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS } diff --git a/linux-wallpaperengine-controller/components/wallpaper/Sidebar.qml b/linux-wallpaperengine-controller/components/wallpaper/Sidebar.qml new file mode 100644 index 000000000..59a2cbee8 --- /dev/null +++ b/linux-wallpaperengine-controller/components/wallpaper/Sidebar.qml @@ -0,0 +1,140 @@ +import QtQuick +import QtQuick.Layouts + +import qs.Commons +import qs.Widgets + +import "." +import "../shared" + +SidebarPanel { + id: root + + // Shared context. + property var pluginApi: null + property var mainInstance: null + + // Current wallpaper preview state. + property var selectedWallpaperData: null + property var propertyCompatibilityBadgeIconForPath: null + property var propertyCompatibilityBadgeTextForPath: null + property var propertyCompatibilityBadgeColorForPath: null + property var propertyCompatibilityBadgeBackgroundForPath: null + property var resolutionBadgeIcon: null + property var resolutionBadgeLabel: null + property var typeLabel: null + property var typeBadgeIcon: null + property var dynamicBadgeIcon: null + property var badgeOrder: [] + property var isVideoMotion: null + property var formatBytes: null + property bool showDescription: true + + // Apply target selection. + property bool singleScreenMode: true + property bool applyAllDisplays: true + property bool applyTargetExpanded: false + property var screenModel: [] + property string selectedScreenName: "" + + // Per-apply runtime options. + property string selectedScaling: "fill" + property string selectedClamp: "clamp" + property int selectedVolume: 100 + property bool selectedMuted: true + property bool selectedAudioReactiveEffects: true + property bool selectedDisableMouse: false + property bool selectedDisableParallax: false + property bool applyWallpaperColorsOnApply: false + property bool applyingWallpaperColors: false + + // Extra wallpaper property editor state. + property bool extraPropertiesEditorEnabled: true + property bool loadingWallpaperProperties: false + property string wallpaperPropertyError: "" + property var wallpaperPropertyDefinitions: [] + property var propertyEditorApi: null + + // Upstream actions. + signal applyRequested() + signal applyAllDisplaysRequested(bool value) + signal applyTargetExpandedRequested(bool value) + signal selectedScreenNameRequested(string value) + signal selectedScalingRequested(string value) + signal selectedClampRequested(string value) + signal selectedVolumeRequested(int value) + signal selectedMutedRequested(bool value) + signal selectedAudioReactiveEffectsRequested(bool value) + signal selectedDisableMouseRequested(bool value) + signal selectedDisableParallaxRequested(bool value) + signal applyWallpaperColorsOnApplyRequested(bool value) + signal sidebarVisibleRequested(bool value) + property bool sidebarVisible: true + + panelVisible: root.selectedWallpaperData !== null && root.sidebarVisible + + // Preview content. + PreviewCard { + pluginApi: root.pluginApi + selectedWallpaperData: root.selectedWallpaperData + propertyCompatibilityBadgeIconForPath: root.propertyCompatibilityBadgeIconForPath + propertyCompatibilityBadgeTextForPath: root.propertyCompatibilityBadgeTextForPath + propertyCompatibilityBadgeColorForPath: root.propertyCompatibilityBadgeColorForPath + propertyCompatibilityBadgeBackgroundForPath: root.propertyCompatibilityBadgeBackgroundForPath + resolutionBadgeIcon: root.resolutionBadgeIcon + resolutionBadgeLabel: root.resolutionBadgeLabel + typeLabel: root.typeLabel + typeBadgeIcon: root.typeBadgeIcon + dynamicBadgeIcon: root.dynamicBadgeIcon + badgeOrder: root.badgeOrder + isVideoMotion: root.isVideoMotion + formatBytes: root.formatBytes + showDescription: root.showDescription + } + + // Apply controls and property editing. + ApplyControls { + pluginApi: root.pluginApi + mainInstance: root.mainInstance + selectedWallpaperData: root.selectedWallpaperData + singleScreenMode: root.singleScreenMode + applyAllDisplays: root.applyAllDisplays + applyTargetExpanded: root.applyTargetExpanded + screenModel: root.screenModel + selectedScreenName: root.selectedScreenName + selectedScaling: root.selectedScaling + selectedClamp: root.selectedClamp + selectedVolume: root.selectedVolume + selectedMuted: root.selectedMuted + selectedAudioReactiveEffects: root.selectedAudioReactiveEffects + selectedDisableMouse: root.selectedDisableMouse + selectedDisableParallax: root.selectedDisableParallax + applyWallpaperColorsOnApply: root.applyWallpaperColorsOnApply + applyingWallpaperColors: root.applyingWallpaperColors + extraPropertiesEditorEnabled: root.extraPropertiesEditorEnabled + loadingWallpaperProperties: root.loadingWallpaperProperties + wallpaperPropertyError: root.wallpaperPropertyError + wallpaperPropertyDefinitions: root.wallpaperPropertyDefinitions + propertyEditorApi: root.propertyEditorApi + onApplyRequested: root.applyRequested() + onApplyAllDisplaysRequested: value => root.applyAllDisplaysRequested(value) + onApplyTargetExpandedRequested: value => root.applyTargetExpandedRequested(value) + onSelectedScreenNameRequested: value => root.selectedScreenNameRequested(value) + onSelectedScalingRequested: value => root.selectedScalingRequested(value) + onSelectedClampRequested: value => root.selectedClampRequested(value) + onSelectedVolumeRequested: value => root.selectedVolumeRequested(value) + onSelectedMutedRequested: value => root.selectedMutedRequested(value) + onSelectedAudioReactiveEffectsRequested: value => root.selectedAudioReactiveEffectsRequested(value) + onSelectedDisableMouseRequested: value => root.selectedDisableMouseRequested(value) + onSelectedDisableParallaxRequested: value => root.selectedDisableParallaxRequested(value) + onApplyWallpaperColorsOnApplyRequested: value => root.applyWallpaperColorsOnApplyRequested(value) + } + + footerContent: NButton { + Layout.fillWidth: true + text: pluginApi?.tr("panel.closeSidebar") + icon: "layout-sidebar-right-collapse" + onClicked: root.sidebarVisibleRequested(false) + } + +} diff --git a/linux-wallpaperengine-controller/helpers/PropertyHelpers.js b/linux-wallpaperengine-controller/helpers/PropertyHelpers.js deleted file mode 100644 index a976c69ca..000000000 --- a/linux-wallpaperengine-controller/helpers/PropertyHelpers.js +++ /dev/null @@ -1,178 +0,0 @@ -.pragma library - -function stripHtml(rawText) { - return String(rawText || "") - .replace(/<[^>]*>/g, " ") - .replace(/ ?/gi, " ") - .replace(/&/gi, "&") - .replace(/</gi, "<") - .replace(/>/gi, ">") - .replace(/\s+/g, " ") - .trim(); -} - -function normalizePropertyLabel(value, translatePropertyLabelKey) { - const raw = String(value || "").trim(); - if (raw.length === 0) { - return ""; - } - - const looksLikeKey = /^[a-z0-9_]+$/i.test(raw) && raw.indexOf("_") >= 0; - if (!looksLikeKey) { - return raw; - } - - const normalizedKey = raw - .replace(/^ui_browse_properties_/i, "") - .replace(/^ui_/i, "") - .replace(/^properties_/i, ""); - - if (normalizedKey.toLowerCase() === "scheme_color" && translatePropertyLabelKey) { - return translatePropertyLabelKey("panel.propertyLabelThemeColor"); - } - - return normalizedKey - .split("_") - .filter(part => part.length > 0) - .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) - .join(" "); -} - -function cleanedPropertyLabel(rawText, fallbackKey, translatePropertyLabelKey) { - const stripped = stripHtml(rawText) - .replace(/^[\-–—•·*_#\s]+/, "") - .replace(/^[^\p{L}\p{N}]+/u, "") - .trim(); - if (stripped.length > 0) { - return normalizePropertyLabel(stripped, translatePropertyLabelKey); - } - return normalizePropertyLabel(String(fallbackKey || ""), translatePropertyLabelKey); -} - -function isNoisePropertyKey(value) { - const key = String(value || "").toLowerCase().trim(); - if (key.length === 0) { - return true; - } - return key.indexOf("imgsrc") === 0 - || key.indexOf("brahref") === 0 - || key.indexOf("centerbrahref") === 0 - || key.indexOf("bigweixin") === 0 - || key.indexOf("viewer_4") >= 0 - || key.indexOf("photogz") >= 0 - || key.indexOf("mqpic") >= 0 - || key.indexOf("width") >= 0 && key.indexOf("height") >= 0; -} - -function isNoisePropertyLabel(value) { - const label = String(value || "").toLowerCase().trim(); - if (label.length === 0) { - return true; - } - return label.indexOf("imgsrc") >= 0 - || label.indexOf("photogz") >= 0 - || label.indexOf("mqpic") >= 0 - || label.indexOf("viewer_4") >= 0; -} - -function comboChoicesFor(definition) { - const rawChoices = definition && definition.choices || []; - const normalized = []; - for (let i = 0; i < rawChoices.length; i++) { - const choice = rawChoices[i]; - const key = String(choice && (choice.key ?? choice.value) || "").trim(); - const name = String(choice && (choice.name ?? choice.label ?? choice.text) || key).trim(); - if (key.length === 0) { - continue; - } - normalized.push({ key: key, name: name.length > 0 ? name : key }); - } - return normalized; -} - -function numberOr(value, fallback) { - const parsed = Number(value); - return isNaN(parsed) ? fallback : parsed; -} - -function formatSliderValue(value, step) { - const numericValue = numberOr(value, 0); - const numericStep = Math.max(numberOr(step, 1), 0.001); - let decimals = 0; - if (numericStep < 1) { - const stepText = String(numericStep); - if (stepText.indexOf("e-") >= 0) { - decimals = Number(stepText.split("e-")[1]) || 0; - } else if (stepText.indexOf(".") >= 0) { - decimals = stepText.split(".")[1].length; - } - } - return numericValue.toFixed(Math.min(decimals, 6)); -} - -function parsePropertyValue(rawValue, type, createColor) { - const trimmed = String(rawValue || "").trim(); - if (type === "boolean") { - return trimmed === "1"; - } - if (type === "slider") { - const parsed = Number(trimmed); - return isNaN(parsed) ? 0 : parsed; - } - if (type === "combo") { - return String(trimmed); - } - if (type === "textinput") { - return trimmed.replace(/^"|"$/g, ""); - } - if (type === "color") { - const parts = trimmed.split(",").map(part => Number(String(part).trim())); - if (parts.length >= 3 && parts.every(part => !isNaN(part))) { - const maxChannel = Math.max(parts[0], parts[1], parts[2]); - if (createColor) { - if (maxChannel > 1) { - return createColor(parts[0] / 255, parts[1] / 255, parts[2] / 255, 1); - } - return createColor(parts[0], parts[1], parts[2], 1); - } - } - return createColor ? createColor(1, 1, 1, 1) : trimmed; - } - return trimmed; -} - -function serializePropertyValue(value, type) { - if (type === "boolean") { - return value ? "1" : "0"; - } - if (type === "slider") { - return String(value); - } - if (type === "combo") { - return String(value); - } - if (type === "textinput") { - return String(value); - } - if (type === "color") { - const color = value; - const r = Math.round((color && color.r !== undefined ? color.r : 1) * 255); - const g = Math.round((color && color.g !== undefined ? color.g : 1) * 255); - const b = Math.round((color && color.b !== undefined ? color.b : 1) * 255); - return String(r) + "," + String(g) + "," + String(b); - } - return String(value); -} - -function ensureColorValue(value, parseColorValue, createColor) { - if (value === undefined || value === null || value === "") { - return createColor ? createColor(1, 1, 1, 1) : value; - } - if (typeof value === "string") { - return parseColorValue ? parseColorValue(value, "color") : value; - } - if (value.r !== undefined && value.g !== undefined && value.b !== undefined) { - return createColor ? createColor(value.r, value.g, value.b, value.a !== undefined ? value.a : 1) : value; - } - return createColor ? createColor(1, 1, 1, 1) : value; -} diff --git a/linux-wallpaperengine-controller/helpers/panel/BadgeHelpers.js b/linux-wallpaperengine-controller/helpers/panel/BadgeHelpers.js new file mode 100644 index 000000000..e1739a764 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/panel/BadgeHelpers.js @@ -0,0 +1,91 @@ +.pragma library + +var allowedBadgeKeys = ["type", "dynamic", "music", "reactive", "approved", "resolution", "compatibility"]; + +function normalizedDefaultOrder(rawOrder) { + const input = Array.isArray(rawOrder) ? rawOrder : allowedBadgeKeys; + const output = []; + for (const raw of input) { + const key = String(raw || "").trim(); + if (allowedBadgeKeys.indexOf(key) >= 0 && output.indexOf(key) < 0) { + output.push(key); + } + } + for (const key of allowedBadgeKeys) { + if (output.indexOf(key) < 0) { + output.push(key); + } + } + return output; +} + +function normalizedDefaultEnabled(rawState) { + const source = rawState && typeof rawState === "object" ? rawState : ({}); + const output = {}; + for (const key of allowedBadgeKeys) { + output[key] = source[key] ?? true; + } + return output; +} + +function normalizeBadgeOrder(rawOrder, fallbackOrder) { + return normalizedDefaultOrder(Array.isArray(rawOrder) ? rawOrder : fallbackOrder); +} + +function normalizeBadgeEnabled(rawState, fallbackState) { + const fallback = normalizedDefaultEnabled(fallbackState); + const source = rawState && typeof rawState === "object" ? rawState : fallback; + const output = {}; + for (const key of allowedBadgeKeys) { + output[key] = source[key] ?? fallback[key] ?? true; + } + return output; +} + +function filterVisibleBadgeOrder(order, enabledState, fallbackState) { + const normalizedOrder = normalizeBadgeOrder(order, allowedBadgeKeys); + const normalizedEnabled = normalizeBadgeEnabled(enabledState, fallbackState); + const output = []; + for (const key of normalizedOrder) { + if (normalizedEnabled[key]) { + output.push(key); + } + } + return output; +} + +function settingsBadgeLabel(key, tr) { + const value = String(key || ""); + if (value === "type") return tr ? tr("settings.badges.items.type") : value; + if (value === "dynamic") return tr ? tr("settings.badges.items.dynamic") : value; + if (value === "music") return tr ? tr("settings.badges.items.music") : value; + if (value === "reactive") return tr ? tr("settings.badges.items.reactive") : value; + if (value === "approved") return tr ? tr("settings.badges.items.approved") : value; + if (value === "resolution") return tr ? tr("settings.badges.items.resolution") : value; + if (value === "compatibility") return tr ? tr("settings.badges.items.compatibility") : value; + return value; +} + +function settingsBadgeIcon(key) { + const value = String(key || ""); + if (value === "type") return "apps"; + if (value === "dynamic") return "player-play"; + if (value === "music") return "volume"; + if (value === "reactive") return "wave-sine"; + if (value === "approved") return "rosette-discount-check"; + if (value === "resolution") return "aspect-ratio"; + if (value === "compatibility") return "settings-cog"; + return "tag"; +} + +function settingsBadgeDescription(key, tr) { + const value = String(key || ""); + if (value === "type") return tr ? tr("settings.badges.details.type") : ""; + if (value === "dynamic") return tr ? tr("settings.badges.details.dynamic") : ""; + if (value === "music") return tr ? tr("settings.badges.details.music") : ""; + if (value === "reactive") return tr ? tr("settings.badges.details.reactive") : ""; + if (value === "approved") return tr ? tr("settings.badges.details.approved") : ""; + if (value === "resolution") return tr ? tr("settings.badges.details.resolution") : ""; + if (value === "compatibility") return tr ? tr("settings.badges.details.compatibility") : ""; + return ""; +} diff --git a/linux-wallpaperengine-controller/helpers/panel/DescriptionHelpers.js b/linux-wallpaperengine-controller/helpers/panel/DescriptionHelpers.js new file mode 100644 index 000000000..3f1c05ccf --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/panel/DescriptionHelpers.js @@ -0,0 +1,158 @@ +.pragma library + +function escapeHtml(text) { + return String(text || "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +function decodeEntities(text) { + return String(text || "") + .replace(/&/gi, "&") + .replace(/"/gi, "\"") + .replace(/'/gi, "'") + .replace(/</gi, "<") + .replace(/>/gi, ">"); +} + +function escapeAttr(text) { + return escapeHtml(String(text || "")).replace(/\n/g, ""); +} + +function normalizeEscapes(rawText) { + let text = String(rawText || ""); + for (let i = 0; i < 4; i++) { + const next = text + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .replace(/\\r\\n/g, "\n") + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\n") + .replace(/\\t/g, " ") + .replace(/\\\//g, "/") + .replace(/\\\"/g, "\"") + .replace(/\\\\/g, "\\"); + + if (next === text) { + break; + } + text = next; + } + return text; +} + +function sanitizeUrl(urlText) { + const raw = decodeEntities(String(urlText || "")).trim(); + if (raw.length === 0) { + return ""; + } + + if (/^(https?:\/\/|steam:\/\/|mailto:)/i.test(raw)) { + return raw; + } + + if (/^www\./i.test(raw)) { + return "https://" + raw; + } + + return ""; +} + +function linkHtml(url, label) { + const safeUrl = sanitizeUrl(url); + if (safeUrl.length === 0) { + return label; + } + return '' + label + ""; +} + +function linkifyPlainUrls(htmlText) { + const parts = String(htmlText || "").split(/(<[^>]+>)/g); + for (let i = 0; i < parts.length; i++) { + const chunk = parts[i]; + if (chunk.length === 0 || chunk.charAt(0) === "<") { + continue; + } + + parts[i] = chunk.replace(/(^|[\s(>])((?:https?:\/\/|www\.)[^\s<]+)/gi, function (_match, prefix, url) { + const link = linkHtml(url, escapeHtml(url)); + if (link === escapeHtml(url)) { + return prefix + escapeHtml(url); + } + return prefix + link; + }); + } + return parts.join(""); +} + +function toRichDescription(rawText) { + const normalized = normalizeEscapes(rawText).trim(); + if (normalized.length === 0) { + return ""; + } + + let text = escapeHtml(normalized); + const blocks = []; + + function stash(blockHtml) { + const token = "@@BLOCK_" + String(blocks.length) + "@@"; + blocks.push(blockHtml); + return token; + } + + text = text.replace(/\[code\]([\s\S]*?)\[\/code\]/gi, function (_m, body) { + return stash('
' + body + "
"); + }); + + text = text.replace(/\[quote(?:=[^\]]+)?\]([\s\S]*?)\[\/quote\]/gi, function (_m, body) { + return stash('
' + body + "
"); + }); + + text = text.replace(/\[img\]([\s\S]*?)\[\/img\]/gi, function (_m, url) { + const label = "[image]"; + const linked = linkHtml(url, escapeHtml(label)); + return linked === escapeHtml(label) ? escapeHtml(label) : linked; + }); + + text = text.replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, function (_m, url, body) { + const inner = String(body || ""); + const linked = linkHtml(url, inner); + return linked === inner ? inner : linked; + }); + + text = text.replace(/\[url\]([\s\S]*?)\[\/url\]/gi, function (_m, url) { + const label = escapeHtml(decodeEntities(String(url || ""))); + const linked = linkHtml(url, label); + return linked === label ? label : linked; + }); + + text = text.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "$1"); + text = text.replace(/\[i\]([\s\S]*?)\[\/i\]/gi, "$1"); + text = text.replace(/\[u\]([\s\S]*?)\[\/u\]/gi, "$1"); + text = text.replace(/\[s\]([\s\S]*?)\[\/s\]/gi, "$1"); + text = text.replace(/\[h1\]([\s\S]*?)\[\/h1\]/gi, "

$1

"); + text = text.replace(/\[h2\]([\s\S]*?)\[\/h2\]/gi, "

$1

"); + text = text.replace(/\[h3\]([\s\S]*?)\[\/h3\]/gi, "

$1

"); + + // Basic list handling: preserve items and drop list wrappers. + text = text + .replace(/\[list(?:=[^\]]*)?\]/gi, "") + .replace(/\[\/list\]/gi, "") + .replace(/\[\*\]/g, "\n- "); + + // Strip unsupported formatting tags but keep content. + text = text.replace(/\[(?:center|left|right|spoiler|size(?:=[^\]]*)?|color(?:=[^\]]*)?|font(?:=[^\]]*)?|table|tr|td)\]/gi, ""); + text = text.replace(/\[\/(?:center|left|right|spoiler|size|color|font|table|tr|td)\]/gi, ""); + + text = linkifyPlainUrls(text); + text = text.replace(/\n{3,}/g, "\n\n").replace(/\n/g, "
"); + + for (let i = 0; i < blocks.length; i++) { + text = text.replace("@@BLOCK_" + String(i) + "@@", blocks[i]); + } + + return text.trim(); +} diff --git a/linux-wallpaperengine-controller/helpers/panel/PropertyHelpers.js b/linux-wallpaperengine-controller/helpers/panel/PropertyHelpers.js new file mode 100644 index 000000000..6691a67a1 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/panel/PropertyHelpers.js @@ -0,0 +1,425 @@ +.pragma library + +function stripHtml(rawText) { + return String(rawText || "") + .replace(/<[^>]*>/g, " ") + .replace(/ ?/gi, " ") + .replace(/&/gi, "&") + .replace(/</gi, "<") + .replace(/>/gi, ">") + .replace(/\s+/g, " ") + .trim(); +} + +function normalizePropertyLabel(value, translatePropertyLabelKey) { + const raw = String(value || "").trim(); + if (raw.length === 0) { + return ""; + } + + const looksLikeKey = /^[a-z0-9_]+$/i.test(raw) && raw.indexOf("_") >= 0; + if (!looksLikeKey) { + return raw; + } + + const normalizedKey = raw + .replace(/^ui_browse_properties_/i, "") + .replace(/^ui_/i, "") + .replace(/^properties_/i, ""); + + if (normalizedKey.toLowerCase() === "scheme_color" && translatePropertyLabelKey) { + return translatePropertyLabelKey("panel.propertyLabelThemeColor"); + } + + return normalizedKey + .split("_") + .filter(part => part.length > 0) + .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join(" "); +} + +function cleanedPropertyLabel(rawText, fallbackKey, translatePropertyLabelKey) { + const stripped = stripHtml(rawText) + .replace(/^[\-–—•·*_#\s]+/, "") + .replace(/^[^\p{L}\p{N}]+/u, "") + .trim(); + if (stripped.length > 0) { + return normalizePropertyLabel(stripped, translatePropertyLabelKey); + } + return normalizePropertyLabel(String(fallbackKey || ""), translatePropertyLabelKey); +} + +function extractImageSourcesFromHtml(rawText) { + const html = String(rawText || ""); + const sources = []; + const imageRegex = /]*\bsrc\s*=\s*['"]?([^'" >]+)[^>]*>/gi; + let match; + while ((match = imageRegex.exec(html)) !== null) { + const source = String(match[1] || "").trim(); + if (source.length > 0) { + sources.push(source); + } + } + return sources; +} + +function isNoisePropertyKey(value) { + const key = String(value || "").toLowerCase().trim(); + if (key.length === 0) { + return true; + } + return key.indexOf("imgsrc") === 0 + || key.indexOf("brahref") === 0 + || key.indexOf("centerbrahref") === 0 + || key.indexOf("bigweixin") === 0 + || key.indexOf("viewer_4") >= 0 + || key.indexOf("photogz") >= 0 + || key.indexOf("mqpic") >= 0 + || key.indexOf("width") >= 0 && key.indexOf("height") >= 0; +} + +function isNoisePropertyLabel(value) { + const label = String(value || "").toLowerCase().trim(); + if (label.length === 0) { + return true; + } + return label.indexOf("imgsrc") >= 0 + || label.indexOf("photogz") >= 0 + || label.indexOf("mqpic") >= 0 + || label.indexOf("viewer_4") >= 0; +} + +function comboChoicesFor(definition) { + const rawChoices = definition && definition.choices || []; + const normalized = []; + for (let i = 0; i < rawChoices.length; i++) { + const choice = rawChoices[i]; + const key = String(choice && (choice.key ?? choice.value) || "").trim(); + const name = String(choice && (choice.name ?? choice.label ?? choice.text) || key).trim(); + if (key.length === 0) { + continue; + } + normalized.push({ key: key, name: name.length > 0 ? name : key }); + } + return normalized; +} + +function numberOr(value, fallback) { + const parsed = Number(value); + return isNaN(parsed) ? fallback : parsed; +} + +function formatSliderValue(value, step) { + const numericValue = numberOr(value, 0); + const numericStep = Math.max(numberOr(step, 1), 0.001); + let decimals = 0; + if (numericStep < 1) { + const stepText = String(numericStep); + if (stepText.indexOf("e-") >= 0) { + decimals = Number(stepText.split("e-")[1]) || 0; + } else if (stepText.indexOf(".") >= 0) { + decimals = stepText.split(".")[1].length; + } + } + return numericValue.toFixed(Math.min(decimals, 6)); +} + +function parsePropertyValue(rawValue, type, createColor) { + const trimmed = String(rawValue || "").trim(); + if (type === "boolean") { + return trimmed === "1"; + } + if (type === "slider") { + const parsed = Number(trimmed); + return isNaN(parsed) ? 0 : parsed; + } + if (type === "combo") { + return String(trimmed); + } + if (type === "textinput" || type === "scene texture") { + return trimmed.replace(/^"|"$/g, ""); + } + if (type === "color") { + const hexMatch = trimmed.match(/^#?([0-9a-f]{6}|[0-9a-f]{8})$/i); + if (hexMatch && createColor) { + const hex = hexMatch[1]; + const hasAlpha = hex.length === 8; + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + const a = hasAlpha ? parseInt(hex.substring(6, 8), 16) / 255 : 1; + return createColor(r, g, b, a); + } + + const parts = trimmed.split(",").map(part => Number(String(part).trim())); + if (parts.length >= 3 && parts.every(part => !isNaN(part))) { + const maxChannel = Math.max(parts[0], parts[1], parts[2]); + const alphaValue = parts.length >= 4 ? parts[3] : 1; + if (createColor) { + if (maxChannel > 1) { + return createColor( + parts[0] / 255, + parts[1] / 255, + parts[2] / 255, + alphaValue > 1 ? alphaValue / 255 : alphaValue + ); + } + return createColor(parts[0], parts[1], parts[2], alphaValue); + } + } + return createColor ? createColor(1, 1, 1, 1) : trimmed; + } + return trimmed; +} + +function serializePropertyValue(value, type) { + if (type === "boolean") { + return value ? "1" : "0"; + } + if (type === "slider") { + return String(value); + } + if (type === "combo") { + return String(value); + } + if (type === "textinput" || type === "scene texture") { + return String(value); + } + if (type === "color") { + const color = value; + const r = Math.round((color && color.r !== undefined ? color.r : 1) * 255); + const g = Math.round((color && color.g !== undefined ? color.g : 1) * 255); + const b = Math.round((color && color.b !== undefined ? color.b : 1) * 255); + const a = Math.round((color && color.a !== undefined ? color.a : 1) * 255); + if (a < 255) { + return String(r) + "," + String(g) + "," + String(b) + "," + String(a); + } + return String(r) + "," + String(g) + "," + String(b); + } + return String(value); +} + +function ensureColorValue(value, parseColorValue, createColor) { + if (value === undefined || value === null || value === "") { + return createColor ? createColor(1, 1, 1, 1) : value; + } + if (typeof value === "string") { + return parseColorValue ? parseColorValue(value, "color") : value; + } + if (value.r !== undefined && value.g !== undefined && value.b !== undefined) { + return createColor ? createColor(value.r, value.g, value.b, value.a !== undefined ? value.a : 1) : value; + } + return createColor ? createColor(1, 1, 1, 1) : value; +} + +function isWritablePropertyType(type) { + const normalizedType = String(type || "").toLowerCase().trim(); + return normalizedType === "boolean" + || normalizedType === "slider" + || normalizedType === "combo" + || normalizedType === "textinput" + || normalizedType === "color"; +} + +function resolvePropertyImageSource(rawValue, wallpaperPath) { + const value = String(rawValue || "").trim().replace(/^"|"$/g, ""); + if (value.length === 0) { + return ""; + } + if (/^(https?:\/\/|file:\/\/|qrc:\/|data:|image:)/i.test(value)) { + return value; + } + if (value.charAt(0) === "/") { + return "file://" + value; + } + + const normalizedWallpaperPath = String(wallpaperPath || "").trim().replace(/\/+$/g, ""); + if (normalizedWallpaperPath.length === 0) { + return value; + } + return "file://" + normalizedWallpaperPath + "/" + value.replace(/^\.?\//, ""); +} + +function parseWallpaperPropertiesOutput(rawText, helpers) { + const lines = String(rawText || "").split(/\r?\n/); + const definitions = []; + const helperApi = helpers || ({}); + const translatePropertyLabelKey = helperApi.translatePropertyLabelKey; + const createColor = helperApi.createColor; + let current = null; + let parsingValues = false; + + function commitCurrent() { + if (!current) { + return; + } + if (["boolean", "slider", "combo", "textinput", "scene texture", "color", "text"].indexOf(current.type) === -1) { + current = null; + parsingValues = false; + return; + } + + const rawLabel = String(current.label || ""); + const imageSources = extractImageSourcesFromHtml(rawLabel); + current.label = cleanedPropertyLabel(rawLabel, current.key, translatePropertyLabelKey); + if (imageSources.length > 0) { + const displayLabel = isNoisePropertyKey(current.key) || isNoisePropertyLabel(current.label) + ? "" + : current.label; + for (let imageIndex = 0; imageIndex < imageSources.length; imageIndex++) { + const imageSource = String(imageSources[imageIndex] || "").trim(); + if (imageSource.length === 0) { + continue; + } + definitions.push({ + key: current.key + "#image" + String(imageIndex), + type: "image", + label: displayLabel, + defaultValue: imageSource, + imageSource: imageSource + }); + } + current = null; + parsingValues = false; + return; + } + + if (current.type === "text") { + if (current.label.length === 0 || isNoisePropertyLabel(current.label)) { + current = null; + parsingValues = false; + return; + } + definitions.push({ + key: current.key, + type: "text", + label: current.label, + defaultValue: "" + }); + current = null; + parsingValues = false; + return; + } + + if (current.type === "scene texture") { + const imageSource = String(current.defaultValue || "").trim().replace(/^"|"$/g, ""); + if (imageSource.length > 0) { + definitions.push({ + key: current.key, + type: "image", + label: current.label, + defaultValue: imageSource, + imageSource: imageSource + }); + } + current = null; + parsingValues = false; + return; + } + + if (isNoisePropertyKey(current.key) || isNoisePropertyLabel(current.label)) { + current = null; + parsingValues = false; + return; + } + + definitions.push(current); + current = null; + parsingValues = false; + } + + for (const rawLine of lines) { + const line = String(rawLine || ""); + const trimmed = line.trim(); + if (trimmed.length === 0) { + commitCurrent(); + continue; + } + + if (trimmed.indexOf("Unknown object type found:") === 0 + || trimmed.indexOf("ScriptEngine [evaluate]:") === 0 + || trimmed.indexOf("Text objects are not supported yet") === 0 + || trimmed.indexOf("Applying override value for ") === 0) { + continue; + } + + const headerMatch = trimmed.match(/^([^\s].*?)\s+-\s+(slider|boolean|combo|textinput|color|text|scene texture)$/i); + if (headerMatch) { + commitCurrent(); + current = { + key: headerMatch[1].trim(), + type: headerMatch[2].toLowerCase(), + label: undefined, + min: undefined, + max: undefined, + step: undefined, + defaultValue: "", + choices: [] + }; + parsingValues = false; + continue; + } + + if (!current) { + continue; + } + + if (trimmed.indexOf("Text:") === 0) { + current.label = trimmed.substring(5).trim(); + parsingValues = false; + continue; + } + if (trimmed.indexOf("Min:") === 0) { + const parsedMin = Number(trimmed.substring(4).trim()); + current.min = isNaN(parsedMin) ? undefined : parsedMin; + parsingValues = false; + continue; + } + if (trimmed.indexOf("Max:") === 0) { + const parsedMax = Number(trimmed.substring(4).trim()); + current.max = isNaN(parsedMax) ? undefined : parsedMax; + parsingValues = false; + continue; + } + if (trimmed.indexOf("Step:") === 0) { + const parsedStep = Number(trimmed.substring(5).trim()); + current.step = isNaN(parsedStep) ? undefined : parsedStep; + parsingValues = false; + continue; + } + if (trimmed.indexOf("Value:") === 0) { + current.defaultValue = parsePropertyValue(trimmed.substring(6).trim(), current.type, createColor); + parsingValues = false; + continue; + } + if (trimmed === "Values:") { + current.choices = []; + parsingValues = true; + continue; + } + + if (parsingValues && current.type === "combo") { + const choiceMatch = trimmed.match(/^(.+?)\s*=\s*(.+)$/); + if (choiceMatch) { + current.choices.push({ + key: choiceMatch[1].trim(), + name: stripHtml(choiceMatch[2]).trim(), + label: stripHtml(choiceMatch[2]).trim(), + value: choiceMatch[1].trim(), + text: stripHtml(choiceMatch[2]).trim() + }); + } else { + current.choices.push({ + key: trimmed, + name: stripHtml(trimmed).trim(), + label: stripHtml(trimmed).trim(), + value: trimmed, + text: stripHtml(trimmed).trim() + }); + } + } + } + + commitCurrent(); + return definitions; +} diff --git a/linux-wallpaperengine-controller/helpers/panel/WallpaperFilterHelpers.js b/linux-wallpaperengine-controller/helpers/panel/WallpaperFilterHelpers.js new file mode 100644 index 000000000..dfc86bdd0 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/panel/WallpaperFilterHelpers.js @@ -0,0 +1,58 @@ +.pragma library + +function filteredAndSortedWallpapers(items, options) { + const sourceItems = Array.isArray(items) ? items.slice() : []; + const filterOptions = options || ({}); + const query = String(filterOptions.query || "").trim().toLowerCase(); + const selectedType = String(filterOptions.selectedType || "all"); + const selectedResolution = String(filterOptions.selectedResolution || "all"); + const sortMode = String(filterOptions.sortMode || "name"); + const sortAscending = filterOptions.sortAscending !== false; + const resolutionFilterKey = filterOptions.resolutionFilterKey; + + let output = sourceItems; + + if (selectedType !== "all") { + output = output.filter(item => String(item.type || "unknown").toLowerCase() === selectedType); + } + + if (selectedResolution !== "all" && resolutionFilterKey) { + output = output.filter(item => resolutionFilterKey(item.resolution) === selectedResolution); + } + + if (query.length > 0) { + output = output.filter(item => { + return String(item.name || "").toLowerCase().indexOf(query) >= 0 + || String(item.id || "").toLowerCase().indexOf(query) >= 0; + }); + } + + if (sortMode === "date") { + output.sort((a, b) => Number(a.mtime || 0) - Number(b.mtime || 0)); + } else if (sortMode === "size") { + output.sort((a, b) => Number(a.bytes || 0) - Number(b.bytes || 0)); + } else if (sortMode === "recent") { + output.sort((a, b) => Number(b.mtime || 0) - Number(a.mtime || 0)); + } else { + output.sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))); + } + + if (!sortAscending) { + output.reverse(); + } + + return output; +} + +function pagedWallpapers(items, currentPage, pageSize) { + const sourceItems = Array.isArray(items) ? items : []; + const safePageSize = Math.max(1, Number(pageSize) || 1); + const totalPages = Math.max(1, Math.ceil(sourceItems.length / safePageSize)); + const nextPage = Math.max(0, Math.min(Number(currentPage) || 0, totalPages - 1)); + const startIndex = nextPage * safePageSize; + + return { + page: nextPage, + items: sourceItems.slice(startIndex, startIndex + safePageSize) + }; +} diff --git a/linux-wallpaperengine-controller/helpers/WallpaperMetaHelpers.js b/linux-wallpaperengine-controller/helpers/panel/WallpaperMetaHelpers.js similarity index 88% rename from linux-wallpaperengine-controller/helpers/WallpaperMetaHelpers.js rename to linux-wallpaperengine-controller/helpers/panel/WallpaperMetaHelpers.js index c620ea705..0da3c8fef 100644 --- a/linux-wallpaperengine-controller/helpers/WallpaperMetaHelpers.js +++ b/linux-wallpaperengine-controller/helpers/panel/WallpaperMetaHelpers.js @@ -5,14 +5,6 @@ function basename(path) { return parts.length > 0 ? parts[parts.length - 1] : ""; } -function workshopUrlForWallpaper(item) { - const wallpaperId = String(item && item.id || "").trim(); - if (!/^\d+$/.test(wallpaperId)) { - return ""; - } - return "https://steamcommunity.com/sharedfiles/filedetails/?id=" + wallpaperId; -} - function fileExt(path) { const raw = basename(path); const idx = raw.lastIndexOf("."); @@ -101,9 +93,6 @@ function resolutionFilterKey(value) { } const longestEdge = Math.max(width, height); - if (longestEdge >= 7680) { - return "8k"; - } if (longestEdge >= 3840) { return "4k"; } diff --git a/linux-wallpaperengine-controller/helpers/panel/WallpaperUiHelpers.js b/linux-wallpaperengine-controller/helpers/panel/WallpaperUiHelpers.js new file mode 100644 index 000000000..6a07d50d9 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/panel/WallpaperUiHelpers.js @@ -0,0 +1,36 @@ +.pragma library + +function typeLabel(value, tr) { + const key = String(value || "all").toLowerCase(); + if (key === "scene") return tr ? tr("panel.typeScene") : key; + if (key === "video") return tr ? tr("panel.typeVideo") : key; + if (key === "web") return tr ? tr("panel.typeWeb") : key; + if (key === "application") return tr ? tr("panel.typeApplication") : key; + return tr ? tr("panel.filterAll") : key; +} + +function typeBadgeIcon(value) { + const key = String(value || "all").toLowerCase(); + if (key === "scene") return "photo"; + if (key === "video") return "video"; + if (key === "web") return "globe"; + if (key === "application") return "apps"; + return "category"; +} + +function dynamicBadgeIcon(isDynamic) { + return isDynamic ? "player-play" : "player-stop"; +} + +function sortLabel(value, tr) { + if (value === "date") return tr ? tr("panel.sortDateAdded") : value; + if (value === "size") return tr ? tr("panel.sortSize") : value; + if (value === "recent") return tr ? tr("panel.sortRecent") : value; + return tr ? tr("panel.sortName") : value; +} + +function resolutionFilterLabel(value, tr) { + if (value === "4k") return tr ? tr("panel.filterRes4k") : value; + if (value === "unknown") return tr ? tr("panel.filterResUnknown") : value; + return tr ? tr("panel.filterResAll") : value; +} diff --git a/linux-wallpaperengine-controller/helpers/runtime/EngineHelpers.js b/linux-wallpaperengine-controller/helpers/runtime/EngineHelpers.js new file mode 100644 index 000000000..c50144c42 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/runtime/EngineHelpers.js @@ -0,0 +1,150 @@ +.pragma library + +function extractRuntimeError(stderrText, messages) { + const text = String(stderrText || "").trim(); + if (text.length === 0) { + return ""; + } + + const errorMessages = messages || ({}); + const lower = text.toLowerCase(); + + if (lower.indexOf("cannot find a valid assets folder") !== -1) { + return String(errorMessages.assetsMissing || ""); + } + + if (lower.indexOf("at least one background id must be specified") !== -1) { + return String(errorMessages.noBackground || ""); + } + + if (lower.indexOf("opengl") !== -1 || lower.indexOf("glfw") !== -1) { + return String(errorMessages.opengl || ""); + } + + const lines = text.split(/\r?\n/) + .map(line => String(line || "").trim()) + .filter(line => line.length > 0); + + if (lines.length === 0) { + return ""; + } + + let summary = lines[0]; + for (const line of lines) { + const normalized = line.toLowerCase(); + if (normalized.indexOf("error") !== -1 || normalized.indexOf("failed") !== -1) { + summary = line; + break; + } + } + + const maxLength = 220; + if (summary.length > maxLength) { + summary = summary.substring(0, maxLength) + "..."; + } + + return summary; +} + +function buildEngineCommand(options) { + const commandOptions = options || ({}); + const command = ["linux-wallpaperengine"]; + let firstPath = ""; + const appendedWallpaperIds = {}; + + command.push("--fps"); + command.push(String(commandOptions.defaultFps)); + + const runtimeClamp = String(commandOptions.defaultClamp || "clamp").trim(); + if (runtimeClamp.length > 0) { + command.push("--clamp"); + command.push(runtimeClamp); + } + + if (commandOptions.defaultMuted) { + command.push("--silent"); + } else { + command.push("--volume"); + command.push(String(commandOptions.defaultVolume)); + } + + if (!commandOptions.defaultAudioReactiveEffects) { + command.push("--no-audio-processing"); + } + + if (commandOptions.defaultNoAutomute) { + command.push("--noautomute"); + } + + if (commandOptions.defaultDisableMouse) { + command.push("--disable-mouse"); + } + + if (commandOptions.defaultDisableParallax) { + command.push("--disable-parallax"); + } + + if (commandOptions.defaultNoFullscreenPause) { + command.push("--no-fullscreen-pause"); + } + + if (commandOptions.defaultFullscreenPauseOnlyActive) { + command.push("--fullscreen-pause-only-active"); + } + + const normalizedAssetsDir = String(commandOptions.assetsDir || "").trim(); + if (normalizedAssetsDir.length > 0) { + command.push("--assets-dir"); + command.push(normalizedAssetsDir); + } + + const screens = commandOptions.screens || []; + const getScreenConfig = commandOptions.getScreenConfig; + const normalizePath = commandOptions.normalizePath; + const wallpaperIdFromPath = commandOptions.wallpaperIdFromPath; + const getWallpaperProperties = commandOptions.getWallpaperProperties; + + for (const screen of screens) { + const screenName = String(screen && screen.name || "").trim(); + if (screenName.length === 0) { + continue; + } + + const screenCfg = getScreenConfig ? getScreenConfig(screenName) : ({}); + const path = normalizePath ? normalizePath(screenCfg.path) : String(screenCfg.path || "").trim(); + if (path.length === 0) { + continue; + } + + if (firstPath.length === 0) { + firstPath = path; + } + + command.push("--screen-root"); + command.push(screenName); + command.push("--bg"); + command.push(path); + command.push("--scaling"); + command.push(String(screenCfg.scaling || commandOptions.defaultScaling || "fill")); + + const wallpaperId = wallpaperIdFromPath ? wallpaperIdFromPath(path) : ""; + if (wallpaperId.length > 0 && !appendedWallpaperIds[wallpaperId]) { + const customProperties = getWallpaperProperties ? getWallpaperProperties(path) : ({}); + for (const propertyKey of Object.keys(customProperties)) { + const propertyValue = customProperties[propertyKey]; + if (propertyValue === undefined || propertyValue === null || String(propertyKey || "").trim().length === 0) { + continue; + } + command.push("--set-property"); + command.push(String(propertyKey) + "=" + String(propertyValue)); + } + appendedWallpaperIds[wallpaperId] = true; + } + } + + if (firstPath.length > 0) { + command.push(firstPath); + } + + return command; +} diff --git a/linux-wallpaperengine-controller/helpers/runtime/WallpaperColorHelpers.js b/linux-wallpaperengine-controller/helpers/runtime/WallpaperColorHelpers.js new file mode 100644 index 000000000..b4bb06462 --- /dev/null +++ b/linux-wallpaperengine-controller/helpers/runtime/WallpaperColorHelpers.js @@ -0,0 +1,90 @@ +.pragma library + +function normalizeWallpaperColorRequest(path, options, defaultScaling, fallbackScreenName, normalizePath) { + const requestOptions = options || ({}); + const normalizedWallpaperPath = normalizePath(String(path || "")); + return { + path: normalizedWallpaperPath, + screenName: String(requestOptions.screenName || fallbackScreenName || "").trim(), + scaling: String(requestOptions.scaling || defaultScaling || "fill").trim(), + notify: !!requestOptions.notify + }; +} + +function buildActiveMonitorSyncRequest(screenName, screenConfig, defaultScaling, normalizePath) { + const normalizedScreenName = String(screenName || "").trim(); + if (normalizedScreenName.length === 0) { + return null; + } + + const wallpaperPath = normalizePath(screenConfig && screenConfig.path || ""); + if (wallpaperPath.length === 0) { + return null; + } + + return { + path: wallpaperPath, + screenName: normalizedScreenName, + scaling: String(screenConfig && screenConfig.scaling || defaultScaling || "fill").trim(), + notify: false + }; +} + +function buildScreenshotCacheEntry(screenshotPath, wallpaperPath, scaling) { + return { + path: String(screenshotPath || "").trim(), + wallpaperPath: String(wallpaperPath || "").trim(), + scaling: String(scaling || "fill").trim(), + updatedAt: Date.now() + }; +} + +function buildReuseCheckRequest(screenName, wallpaperPath, scaling, screenshotPath, notify) { + return { + wallpaperPath: String(wallpaperPath || "").trim(), + screenName: String(screenName || "").trim(), + scaling: String(scaling || "fill").trim(), + screenshotPath: String(screenshotPath || "").trim(), + notify: !!notify + }; +} + +function buildCaptureCommand(scriptPath, screenshotPath, assetsDir, defaultFps, defaultClamp, targetScreenName, wallpaperPath, targetScaling, wallpaperProperties) { + const command = [ + "bash", + scriptPath, + screenshotPath, + "linux-wallpaperengine" + ]; + + const normalizedAssetsDir = String(assetsDir || "").trim(); + if (normalizedAssetsDir.length > 0) { + command.push("--assets-dir"); + command.push(normalizedAssetsDir); + } + + command.push("--fps"); + command.push(String(defaultFps)); + command.push("--clamp"); + command.push(String(defaultClamp || "clamp")); + command.push("--screen-root"); + command.push(String(targetScreenName || "").trim()); + command.push("--bg"); + command.push(String(wallpaperPath || "").trim()); + command.push("--scaling"); + command.push(String(targetScaling || "fill").trim() || "fill"); + command.push("--screenshot"); + command.push(String(screenshotPath || "").trim()); + + const customProperties = wallpaperProperties || ({}); + for (const propertyKey of Object.keys(customProperties)) { + const propertyValue = customProperties[propertyKey]; + if (propertyValue === undefined || propertyValue === null || String(propertyKey || "").trim().length === 0) { + continue; + } + command.push("--set-property"); + command.push(String(propertyKey) + "=" + String(propertyValue)); + } + + return command; +} diff --git a/linux-wallpaperengine-controller/helpers/ColorCacheHelpers.js b/linux-wallpaperengine-controller/helpers/shared/ColorCacheHelpers.js similarity index 100% rename from linux-wallpaperengine-controller/helpers/ColorCacheHelpers.js rename to linux-wallpaperengine-controller/helpers/shared/ColorCacheHelpers.js diff --git a/linux-wallpaperengine-controller/i18n/en.json b/linux-wallpaperengine-controller/i18n/en.json index be9954e2f..e684d6cb6 100644 --- a/linux-wallpaperengine-controller/i18n/en.json +++ b/linux-wallpaperengine-controller/i18n/en.json @@ -17,10 +17,8 @@ "reloaded": "Wallpaper engine reloaded", "stopped": "Wallpaper engine stopped", "reloadSkippedNoWallpaper": "No wallpaper is configured, so there was nothing to reload", - "refreshingWallpapers": "Refreshing wallpaper list...", "refreshedWallpapers": "Wallpaper list refreshed: {count} wallpapers", "refreshSkippedNoFolder": "Wallpaper source directory is empty, so the wallpaper list could not be refreshed", - "wallpaperColorsGenerating": "Generating wallpaper screenshot for color extraction...", "wallpaperColorsApplied": "Wallpaper colors applied to Noctalia", "wallpaperColorsCached": "Wallpaper colors cached for this display", "wallpaperColorsFailed": "Failed to extract colors from the selected wallpaper", @@ -28,13 +26,6 @@ "wallpaperColorsEngineUnavailable": "linux-wallpaperengine must be available to generate wallpaper colors" }, "main": { - "status": { - "unavailable": "Engine unavailable", - "ready": "Ready", - "starting": "Starting engine", - "stopped": "Stopped", - "crashed": "Engine exited unexpectedly" - }, "error": { "notInstalled": "linux-wallpaperengine was not found in PATH. Please check whether it is installed correctly.", "assetsMissing": "Wallpaper Engine assets were not found. Configure the assets directory in settings.", @@ -45,36 +36,30 @@ }, "panel": { "title": "Wallpaper-Engine Wallpaper Selector", - "statusChecking": "Checking", - "statusUnavailable": "Unavailable", - "statusRunning": "Running", - "statusReady": "Configured", - "statusStopped": "Stopped", "sectionAudio": "Display & Audio", "sectionFeatures": "Features", "installHint": "Install example: yay -S linux-wallpaperengine-git", "errorBannerTitle": "Runtime error detected", "errorShowDetails": "View details", "errorHideDetails": "Hide details", + "errorCopy": "Copy error", + "errorCopied": "Error details copied", "errorDismiss": "Dismiss", "refreshWallpapers": "Refresh wallpapers", "start": "Start", "stop": "Stop", - "confirm": "Confirm", - "cancel": "Cancel", "compatibilityQuickCheck": "Compatibility quick check", - "compatibilityQuickCheckRunning": "Checking wallpaper compatibility...", - "compatibilityQuickCheckConfirm": "Scan all wallpapers for extra property compatibility? This may take some time.", - "compatibilityQuickCheckFinished": "Scanned {total} wallpapers, {failed} may fail", + "compatibilityQuickCheckFinished": "Scanned {total} wallpapers, {failed} may fail, {limited} limited", "folderInvalid": "Wallpaper source directory is invalid or inaccessible.", "scanning": "Scanning wallpapers...", "searchPlaceholder": "Search wallpapers by name or ID", "searchClear": "Clear search", - "filterButtonSummary": "Filter · {type}", + "showMore": "Show more", + "showLess": "Show less", + "filterButtonSummary": "Filter: {type}", "sortButtonSummary": "Sort · {direction} {sort}", "applyAllDisplays": "Apply to all displays", - "closePanel": "Close panel", - "confirmApply": "Apply wallpaper", + "closeSidebar": "Close sidebar", "syncWallpaperColors": "Sync wallpaper colors", "wallpaperScaling": "Scaling", "wallpaperClamp": "Clamp mode", @@ -87,10 +72,12 @@ "loadingProperties": "Loading wallpaper properties...", "propertiesLoadFailed": "Failed to load wallpaper properties. This wallpaper format may not be supported yet, or this wallpaper may not run correctly", "propertiesFailedBadge": "May fail", + "propertiesLimitedBadge": "Limited", "noEditableProperties": "This wallpaper has no editable extra properties yet", "propertiesNotice": "Only directly editable properties supported by this plugin are shown here", "infoType": "Type", "infoId": "ID", + "infoDescription": "Description", "infoResolution": "Resolution", "infoSize": "Size", "scalingFill": "Fill", @@ -104,7 +91,6 @@ "targetAllDisplays": "Target: All displays", "targetSingleDisplay": "Target: {screen}", "applySingleDisplay": "Apply to {screen}", - "applyTarget": "Choose apply target", "filterAll": "All", "filterTypeAll": "All", "filterTypeScene": "Scene", @@ -112,18 +98,13 @@ "filterTypeWeb": "Web", "filterTypeApplication": "Application", "filterResAll": "Resolution: All", - "filterRes4k": "Resolution: 4K", - "filterRes8k": "Resolution: 8K", + "filterRes4k": "Resolution: 4K+", "filterResUnknown": "Resolution: Unknown", - "filterStatic": "Static", - "filterDynamic": "Dynamic", "sortName": "Name", "sortDateAdded": "Added time", "sortSize": "Size", "sortRecent": "Recent", - "sortAscendingToggle": "Toggle Asc/Desc", "sortAscendingToggleWithDirection": "{direction} Toggle Asc/Desc", - "sortId": "ID", "typeScene": "Scene", "typeVideo": "Video", "typeWeb": "Web", @@ -131,13 +112,15 @@ "resolutionUnknown": "Unknown", "dynamicBadge": "Dynamic", "staticBadge": "Static", + "musicBadge": "Music", + "reactiveBadge": "Reactive", "prevPage": "Previous", "nextPage": "Next", "pageSummary": "Page {current} / {total}", "pageRange": "{start}-{end} / {total}", - "count": "Wallpaper count: {count}", "emptyAll": "No wallpapers found in the source directory", - "emptyFiltered": "No wallpapers match the current filters" + "emptyFiltered": "No wallpapers match the current filters", + "approvedBadge": "Recommended" }, "settings": { "category": { @@ -147,6 +130,7 @@ "audioTitle": "Audio", "displayTitle": "Display" }, + "resourcesIntro": "Manage wallpaper source scanning and the cached images used for color extraction.", "defaults": { "title": "Default application options", "description": "These options are used as defaults when you apply a wallpaper without overriding its per-wallpaper settings." @@ -159,6 +143,25 @@ "label": "Enable extra property editor", "description": "Show wallpaper-specific extra properties in the panel sidebar when available" }, + "showSidebarDescription": { + "label": "Show sidebar description", + "description": "Display wallpaper descriptions in the sidebar preview card" + }, + "badges": { + "title": "Wallpaper card badge order", + "description": "Drag or use arrows to reorder badges, then toggle each one on or off.", + "moveUp": "Move up", + "moveDown": "Move down", + "items": { + "type": "Type", + "dynamic": "Dynamic/Static", + "music": "Music", + "reactive": "Reactive", + "approved": "Recommended", + "resolution": "Resolution", + "compatibility": "Compatibility" + } + }, "cache": { "currentSize": "Current color image cache size: {size}", "refresh": "Refresh color cache size", @@ -176,11 +179,6 @@ "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", "scan": "Auto-detect Steam workshop directory" }, - "assetsDir": { - "label": "Wallpaper Engine assets directory", - "description": "Optional: assets path used by linux-wallpaperengine", - "placeholder": "~/.local/share/wallpaperengine/assets" - }, "wallpaperScanCacheMinutes": { "label": "Wallpaper scan cache duration", "description": "Reuse the cached wallpaper list for this many minutes before scanning again when opening the panel" @@ -195,8 +193,7 @@ }, "defaultFps": { "label": "FPS limit", - "description": "Default frame rate limit for wallpapers", - "placeholder": "30" + "description": "Default frame rate limit for wallpapers" }, "defaultNoFullscreenPause": { "label": "Prevent fullscreen pause", diff --git a/linux-wallpaperengine-controller/i18n/ja.json b/linux-wallpaperengine-controller/i18n/ja.json index 7c570aedf..453a389a9 100644 --- a/linux-wallpaperengine-controller/i18n/ja.json +++ b/linux-wallpaperengine-controller/i18n/ja.json @@ -1,238 +1,235 @@ { - "widget": { - "tooltip": { - "checking": "linux-wallpaperengine を確認中", - "unavailable": "linux-wallpaperengine がインストールされていません", - "running": "壁紙エンジンが設定を適用中です", - "ready": "壁紙エンジンパネルを開く" - } - }, - "menu": { - "refreshWallpapers": "壁紙一覧を更新", - "start": "壁紙エンジンを開始", - "stop": "壁紙エンジンを停止", - "settings": "プラグイン設定" - }, - "toast": { - "reloaded": "壁紙エンジンを再読み込みしました", - "stopped": "壁紙エンジンを停止しました", - "reloadSkippedNoWallpaper": "壁紙が設定されていないため、再読み込みする内容がありません", - "refreshingWallpapers": "壁紙一覧を更新中...", - "refreshedWallpapers": "壁紙一覧を更新しました:{count} 件", - "refreshSkippedNoFolder": "壁紙ソースディレクトリが空のため、壁紙一覧を更新できません", - "wallpaperColorsGenerating": "色抽出用の壁紙スクリーンショットを生成中...", - "wallpaperColorsApplied": "壁紙の色を Noctalia に適用しました", - "wallpaperColorsCached": "このディスプレイ用の壁紙カラーを保存しました", - "wallpaperColorsFailed": "選択した壁紙から色を抽出できませんでした", - "wallpaperColorsNoSelection": "壁紙の色を適用する前に壁紙を選択してください", - "wallpaperColorsEngineUnavailable": "壁紙の色を生成するには linux-wallpaperengine が利用可能である必要があります" - }, "main": { - "status": { - "unavailable": "エンジンは利用できません", - "ready": "準備完了", - "starting": "エンジンを起動中", - "stopped": "停止しました", - "crashed": "エンジンが異常終了しました" - }, "error": { - "notInstalled": "PATH に linux-wallpaperengine が見つかりません。正しくインストールされているか確認してください。", "assetsMissing": "Wallpaper Engine の assets が見つかりません。設定で assets ディレクトリを指定してください。", + "autoRecovered": "前回利用可能だったレイアウトに自動復旧しました", "noBackground": "壁紙パスが設定されていません。", - "opengl": "OpenGL の初期化に失敗しました。ドライバーとコンポジタの互換性を確認してください。", - "autoRecovered": "前回利用可能だったレイアウトに自動復旧しました" + "notInstalled": "PATH に linux-wallpaperengine が見つかりません。正しくインストールされているか確認してください。", + "opengl": "OpenGL の初期化に失敗しました。ドライバーとコンポジタの互換性を確認してください。" } }, + "menu": { + "refreshWallpapers": "壁紙一覧を更新", + "settings": "プラグイン設定", + "start": "壁紙エンジンを開始", + "stop": "壁紙エンジンを停止" + }, "panel": { - "title": "Wallpaper-Engine 壁紙セレクター", - "statusChecking": "確認中", - "statusUnavailable": "利用不可", - "statusRunning": "実行中", - "statusReady": "設定済み", - "statusStopped": "停止", - "sectionAudio": "ディスプレイとオーディオ", - "sectionFeatures": "機能", - "installHint": "インストール例: yay -S linux-wallpaperengine-git", + "applyAllDisplays": "すべてのディスプレイに適用", + "applySingleDisplay": "{screen} に適用", + "approvedBadge": "おすすめ", + "clampBorder": "境界", + "clampClamp": "クランプ", + "clampRepeat": "繰り返し", + "closeSidebar": "サイドバーを閉じる", + "compatibilityQuickCheck": "互換性クイックチェック", + "compatibilityQuickCheckFinished": "{total} 件の壁紙をスキャンし、{failed} 件が要注意、{limited} 件が制限付きです", + "dynamicBadge": "動的", + "emptyAll": "ソースディレクトリに壁紙が見つかりません", + "emptyFiltered": "条件に一致する壁紙が見つかりません", "errorBannerTitle": "ランタイムエラーを検出", - "errorShowDetails": "詳細を表示", - "errorHideDetails": "詳細を隠す", + "errorCopied": "エラー詳細をコピーしました", + "errorCopy": "エラーをコピー", "errorDismiss": "閉じる", - "refreshWallpapers": "壁紙を更新", - "start": "開始", - "stop": "停止", - "confirm": "確認", - "cancel": "キャンセル", - "compatibilityQuickCheck": "互換性クイックチェック", - "compatibilityQuickCheckRunning": "壁紙の互換性を確認中...", - "compatibilityQuickCheckConfirm": "すべての壁紙の追加プロパティ互換性をスキャンしますか? 少し時間がかかる場合があります。", - "compatibilityQuickCheckFinished": "{total} 件の壁紙をスキャンし、{failed} 件が要注意です", + "errorHideDetails": "詳細を隠す", + "errorShowDetails": "詳細を表示", + "filterAll": "すべて", + "filterButtonSummary": "フィルター:{type}", + "filterRes4k": "解像度: 4K+", + "filterResAll": "解像度: すべて", + "filterResUnknown": "解像度: 不明", + "filterTypeAll": "すべて", + "filterTypeApplication": "アプリケーション", + "filterTypeScene": "シーン", + "filterTypeVideo": "動画", + "filterTypeWeb": "ウェブ", "folderInvalid": "壁紙ソースディレクトリが無効、またはアクセスできません。", - "scanning": "壁紙をスキャン中...", - "searchPlaceholder": "名前または ID で壁紙を検索", - "searchClear": "検索をクリア", - "filterButtonSummary": "フィルター · {type}", - "sortButtonSummary": "並び替え · {direction}{sort}", - "applyAllDisplays": "すべてのディスプレイに適用", - "closePanel": "パネルを閉じる", - "confirmApply": "壁紙を適用", - "syncWallpaperColors": "壁紙の色を同期", - "wallpaperScaling": "拡大縮小", - "wallpaperClamp": "クランプモード", - "wallpaperVolume": "音量", - "wallpaperMuted": "ミュート", - "wallpaperAudioReactive": "オーディオ反応効果", - "wallpaperDisableMouse": "マウス操作を無効化", - "wallpaperDisableParallax": "パララックス効果を無効化", - "sectionProperties": "追加プロパティ", - "loadingProperties": "壁紙プロパティを読み込み中...", - "propertiesLoadFailed": "壁紙プロパティの読み込みに失敗しました。この壁紙のプロパティ形式はまだサポートされていない可能性があるか、この壁紙自体が正しく読み込めず実行できない可能性があります", - "propertiesFailedBadge": "要注意", - "noEditableProperties": "この壁紙には、今のところ編集可能な追加プロパティがありません", - "propertiesNotice": "ここには、このプラグインで直接編集できるプロパティのみ表示されます", - "infoType": "タイプ", "infoId": "ID", + "infoDescription": "説明", "infoResolution": "解像度", "infoSize": "サイズ", + "infoType": "タイプ", + "installHint": "インストール例: yay -S linux-wallpaperengine-git", + "loadingProperties": "壁紙プロパティを読み込み中...", + "musicBadge": "音楽", + "nextPage": "次へ", + "noEditableProperties": "この壁紙には、今のところ編集可能な追加プロパティがありません", + "pageRange": "{start}-{end} / {total}", + "pageSummary": "{current} / {total} ページ", + "prevPage": "前へ", + "propertiesFailedBadge": "要注意", + "propertiesLimitedBadge": "制限あり", + "propertiesLoadFailed": "壁紙プロパティの読み込みに失敗しました。この壁紙のプロパティ形式はまだサポートされていない可能性があるか、この壁紙自体が正しく読み込めず実行できない可能性があります", + "propertiesNotice": "ここには、このプラグインで直接編集できるプロパティのみ表示されます", + "propertyLabelThemeColor": "テーマカラー", + "reactiveBadge": "オーディオ反応", + "refreshWallpapers": "壁紙を更新", + "resolutionUnknown": "不明", + "scalingDefault": "既定", "scalingFill": "フィル", "scalingFit": "フィット", "scalingStretch": "ストレッチ", - "scalingDefault": "既定", - "clampClamp": "クランプ", - "clampBorder": "境界", - "clampRepeat": "繰り返し", - "propertyLabelThemeColor": "テーマカラー", - "targetAllDisplays": "対象: すべてのディスプレイ", - "targetSingleDisplay": "対象: {screen}", - "applySingleDisplay": "{screen} に適用", - "applyTarget": "適用先を選択", - "filterAll": "すべて", - "filterTypeAll": "すべて", - "filterTypeScene": "シーン", - "filterTypeVideo": "動画", - "filterTypeWeb": "ウェブ", - "filterTypeApplication": "アプリケーション", - "filterResAll": "解像度: すべて", - "filterRes4k": "解像度: 4K", - "filterRes8k": "解像度: 8K", - "filterResUnknown": "解像度: 不明", - "filterStatic": "静的", - "filterDynamic": "動的", - "sortName": "名前", + "scanning": "壁紙をスキャン中...", + "searchClear": "検索をクリア", + "searchPlaceholder": "名前または ID で壁紙を検索", + "showMore": "もっと見る", + "showLess": "折りたたむ", + "sectionAudio": "ディスプレイとオーディオ", + "sectionFeatures": "機能", + "sectionProperties": "追加プロパティ", + "sortAscendingToggleWithDirection": "{direction} 昇順/降順を切替", + "sortButtonSummary": "並び替え · {direction}{sort}", "sortDateAdded": "追加時間", + "sortName": "名前", "sortSize": "サイズ", "sortRecent": "最近", - "sortAscendingToggle": "昇順/降順を切替", - "sortAscendingToggleWithDirection": "{direction} 昇順/降順を切替", - "sortId": "ID", + "start": "開始", + "staticBadge": "静的", + "stop": "停止", + "syncWallpaperColors": "壁紙の色を同期", + "targetAllDisplays": "対象: すべてのディスプレイ", + "targetSingleDisplay": "対象: {screen}", + "title": "Wallpaper-Engine 壁紙セレクター", + "typeApplication": "アプリケーション", "typeScene": "シーン", "typeVideo": "動画", "typeWeb": "ウェブ", - "typeApplication": "アプリケーション", - "resolutionUnknown": "不明", - "dynamicBadge": "動的", - "staticBadge": "静的", - "prevPage": "前へ", - "nextPage": "次へ", - "pageSummary": "{current} / {total} ページ", - "pageRange": "{start}-{end} / {total}", - "count": "壁紙数: {count}", - "emptyAll": "ソースディレクトリに壁紙が見つかりません", - "emptyFiltered": "条件に一致する壁紙が見つかりません" + "wallpaperAudioReactive": "オーディオ反応効果", + "wallpaperClamp": "クランプモード", + "wallpaperDisableMouse": "マウス操作を無効化", + "wallpaperDisableParallax": "パララックス効果を無効化", + "wallpaperMuted": "ミュート", + "wallpaperScaling": "拡大縮小", + "wallpaperVolume": "音量" }, "settings": { - "category": { - "interfaceTitle": "インターフェース", - "performanceTitle": "パフォーマンス", - "compatibilityTitle": "リソース", - "audioTitle": "音声", - "displayTitle": "表示" - }, - "defaults": { - "title": "既定の適用オプション", - "description": "壁紙ごとの設定を上書きしない場合、壁紙の適用時にこれらの項目が既定値として使用されます。" - }, - "iconColor": { - "label": "アイコンの色", - "description": "バーウィジェットのアイコンに使う色" - }, - "enableExtraPropertiesEditor": { - "label": "追加プロパティ編集を有効化", - "description": "利用可能な場合、壁紙ごとの追加プロパティをパネルのサイドバーに表示します" + "autoApplyOnStartup": { + "description": "少なくとも 1 つのスクリーンに壁紙が設定されている場合、起動時にエンジンを自動実行", + "label": "起動時に自動適用" }, "cache": { + "clear": "配色画像キャッシュを削除", "currentSize": "現在の配色画像キャッシュサイズ: {size}", "refresh": "配色キャッシュサイズを更新", - "clear": "配色画像キャッシュを削除", "sizeUnknown": "不明" }, - "units": { - "fps": " FPS", - "percent": " %", - "minutes": " 分" + "defaults": { + "title": "デフォルト値", + "description": "以下の設定は新しい壁紙のデフォルト値のみです。既に設定されている wallpaperには影響しません" }, - "wallpapersFolder": { - "label": "壁紙ソースディレクトリ", - "description": "壁紙プロジェクトフォルダーを含むディレクトリ", - "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", - "scan": "Steam ワークショップディレクトリを自動検出" + "category": { + "audioTitle": "音声", + "compatibilityTitle": "リソース", + "displayTitle": "表示", + "interfaceTitle": "インターフェース", + "performanceTitle": "パフォーマンス" }, - "assetsDir": { - "label": "Wallpaper Engine 資産ディレクトリ", - "description": "任意: linux-wallpaperengine が使用する assets パス", - "placeholder": "~/.local/share/wallpaperengine/assets" + "resourcesIntro": "壁紙ソースのスキャンと、色抽出に使うキャッシュ画像を管理します。", + "defaultAudioReactiveEffects": { + "description": "該機能をサポートする壁紙の既定でオーディオ処理を有効にしたままにします", + "label": "オーディオ反応効果" }, - "wallpaperScanCacheMinutes": { - "label": "壁紙スキャンキャッシュ時間", - "description": "パネルを開くとき、この分数を超えていなければキャッシュ済みの壁紙一覧を再利用します" + "defaultClamp": { + "description": "壁紙を描画するときに使う既定の境界処理モード", + "label": "既定のクランプモード" }, - "defaultScaling": { - "label": "拡大縮小モード", - "description": "カスタム値が未設定のときに使う既定モード" + "defaultDisableMouse": { + "description": "既定でマウス入力を無効化した状態で壁紙を開始", + "label": "マウス操作を無効化" }, - "defaultClamp": { - "label": "クランプモード", - "description": "壁紙を描画するときに使う既定の境界処理モード" + "defaultDisableParallax": { + "description": "既定でパララックス効果を無効化した状態で壁紙を開始", + "label": "パララックス効果を無効化" }, "defaultFps": { - "label": "FPS 制限", "description": "壁紙で使う既定のフレームレート上限", - "placeholder": "30" + "label": "既定 FPS" + }, + "defaultFullscreenPauseOnlyActive": { + "description": "(Wayland のみ)フルスクリーンウィンドウがアクティブなときのみ一時停止", + "label": "アクティブなフルスクリーン時のみ一時停止" + }, + "defaultMuted": { + "description": "壁紙を既定でミュート状態で起動します", + "label": "壁紙を既定でミュート" + }, + "defaultNoAutomute": { + "description": "有効にすると、他のアプリが音を出していても壁紙音声を再生し続けます", + "label": "他のアプリが音声再生中でもミュートしない" }, "defaultNoFullscreenPause": { - "label": "フルスクリーン時の一時停止を防止", - "description": "フルスクリーンアプリ実行中に一時停止しない" + "description": "フルスクリーンアプリ実行中に一時停止しない", + "label": "フルスクリーン時の一時停止を防止" }, - "defaultFullscreenPauseOnlyActive": { - "label": "アクティブなフルスクリーン時のみ一時停止", - "description": "(Wayland のみ)フルスクリーンウィンドウがアクティブなときのみ一時停止" + "defaultScaling": { + "description": "カスタム値が未設定のときに使う既定モード", + "label": "既定の拡大縮小モード" }, "defaultVolume": { - "label": "音量", - "description": "壁紙をミュートせずに起動したときに使う音量です" - }, - "defaultMuted": { - "label": "ミュートで起動", - "description": "壁紙を既定でミュート状態で起動します" + "description": "ミュートでない場合に、壁紙起動時に使用される音量", + "label": "壁紙の既定音量" }, - "defaultAudioReactiveEffects": { - "label": "オーディオ反応効果", - "description": "壁紙が対応している場合、音声処理を既定で有効のままにします" + "enableExtraPropertiesEditor": { + "description": "利用可能な場合、壁紙ごとの追加プロパティをパネルのサイドバーに表示します", + "label": "追加プロパティ編集を有効化" + }, + "showSidebarDescription": { + "description": "サイドバーのプレビューカードに壁紙の説明を表示します", + "label": "サイドバーに説明を表示" + }, + "badges": { + "title": "壁紙カード Badge 表示順", + "description": "ドラッグまたは矢印で順序を変え、各 Badge を個別に切り替えます。", + "moveUp": "上へ", + "moveDown": "下へ", + "items": { + "type": "タイプ", + "dynamic": "動的/静的", + "music": "音楽", + "reactive": "オーディオ反応", + "approved": "おすすめ", + "resolution": "解像度", + "compatibility": "互換性" + } }, - "defaultNoAutomute": { - "label": "他のアプリが音声再生中でもミュートしない", - "description": "有効にすると、他のアプリが音を出していても壁紙音声を再生し続けます" + "iconColor": { + "description": "バーウィジェットのアイコンに使う色", + "label": "アイコンの色" }, - "defaultDisableMouse": { - "label": "マウス操作を無効化", - "description": "壁紙を既定でマウス入力無効の状態で起動します" + "units": { + "fps": " FPS", + "minutes": " 分", + "percent": " %" }, - "defaultDisableParallax": { - "label": "パララックス効果を無効化", - "description": "壁紙を既定でパララックス無効の状態で起動します" + "wallpaperScanCacheMinutes": { + "description": "パネルを開くとき、この分数を超えていなければキャッシュ済みの壁紙一覧を再利用します", + "label": "壁紙スキャンキャッシュ時間" }, - "autoApplyOnStartup": { - "label": "起動時に自動適用", - "description": "少なくとも 1 つのスクリーンに壁紙が設定されている場合、起動時にエンジンを自動実行" + "wallpapersFolder": { + "description": "壁纸プロジェクトフォルダーを含むディレクトリ", + "label": "壁纸ソースディレクトリ", + "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", + "scan": "Steam ワークショップディレクトリを自動検出" + } + }, + "toast": { + "refreshSkippedNoFolder": "壁紙ソースディレクトリが空のため、壁紙一覧を更新できません", + "refreshedWallpapers": "壁紙一覧を更新しました:{count} 件", + "reloadSkippedNoWallpaper": "壁紙が設定されていないため、再読み込みする内容がありません", + "reloaded": "壁紙エンジンを再読み込みしました", + "stopped": "壁紙エンジンを停止しました", + "wallpaperColorsApplied": "壁紙の色を Noctalia に適用しました", + "wallpaperColorsCached": "このディスプレイ用の壁紙カラーを保存しました", + "wallpaperColorsEngineUnavailable": "壁紙の色を生成するには linux-wallpaperengine が利用可能である必要があります", + "wallpaperColorsFailed": "選択した壁紙から色を抽出できませんでした", + "wallpaperColorsNoSelection": "壁紙の色を適用する前に壁紙を選択してください" + }, + "widget": { + "tooltip": { + "checking": "linux-wallpaperengine を確認中", + "ready": "壁紙エンジンパネルを開く", + "running": "壁紙エンジンが設定を適用中です", + "unavailable": "linux-wallpaperengine がインストールされていません" } } } diff --git a/linux-wallpaperengine-controller/i18n/ru.json b/linux-wallpaperengine-controller/i18n/ru.json index d67c7d584..8f3cc8663 100644 --- a/linux-wallpaperengine-controller/i18n/ru.json +++ b/linux-wallpaperengine-controller/i18n/ru.json @@ -1,238 +1,235 @@ { - "widget": { - "tooltip": { - "checking": "Проверка linux-wallpaperengine", - "unavailable": "linux-wallpaperengine не установлен", - "running": "Движок обоев применяет настройки", - "ready": "Открыть панель движка обоев" - } - }, - "menu": { - "refreshWallpapers": "Обновить список обоев", - "start": "Запустить движок обоев", - "stop": "Остановить движок обоев", - "settings": "Настройки плагина" - }, - "toast": { - "reloaded": "Движок обоев перезагружен", - "stopped": "Движок обоев остановлен", - "reloadSkippedNoWallpaper": "Обои не настроены, поэтому перезагружать нечего", - "refreshingWallpapers": "Обновление списка обоев...", - "refreshedWallpapers": "Список обоев обновлён: {count}", - "refreshSkippedNoFolder": "Каталог источника обоев пуст, поэтому список обоев нельзя обновить", - "wallpaperColorsGenerating": "Создание скриншота обоев для извлечения цветов...", - "wallpaperColorsApplied": "Цвета обоев применены к Noctalia", - "wallpaperColorsCached": "Цвета обоев сохранены для этого дисплея", - "wallpaperColorsFailed": "Не удалось извлечь цвета из выбранных обоев", - "wallpaperColorsNoSelection": "Сначала выберите обои, а затем применяйте их цвета", - "wallpaperColorsEngineUnavailable": "Для генерации цветов обоев требуется доступный linux-wallpaperengine" - }, "main": { - "status": { - "unavailable": "Движок недоступен", - "ready": "Готово", - "starting": "Запуск движка", - "stopped": "Остановлено", - "crashed": "Движок неожиданно завершился" - }, "error": { - "notInstalled": "linux-wallpaperengine не найден в PATH. Проверьте, установлен ли он правильно.", "assetsMissing": "Ресурсы Wallpaper Engine не найдены. Укажите каталог ресурсов в настройках.", + "autoRecovered": "Автоматически восстановлена последняя рабочая раскладка", "noBackground": "Путь к обоям не настроен.", - "opengl": "Не удалось инициализировать OpenGL. Проверьте драйвер и совместимость композитора.", - "autoRecovered": "Автоматически восстановлена последняя рабочая раскладка" + "notInstalled": "linux-wallpaperengine не найден в PATH. Проверьте, установлен ли он правильно.", + "opengl": "Не удалось инициализировать OpenGL. Проверьте драйвер и совместимость композитора." } }, + "menu": { + "refreshWallpapers": "Обновить список обоев", + "settings": "Настройки плагина", + "start": "Запустить движок обоев", + "stop": "Остановить движок обоев" + }, "panel": { - "title": "Wallpaper-Engine Выбор обоев", - "statusChecking": "Проверка", - "statusUnavailable": "Недоступно", - "statusRunning": "Работает", - "statusReady": "Настроено", - "statusStopped": "Остановлено", - "sectionAudio": "Отображение и аудио", - "sectionFeatures": "Функции", - "installHint": "Пример установки: yay -S linux-wallpaperengine-git", + "applyAllDisplays": "Применить ко всем дисплеям", + "applySingleDisplay": "Применить к {screen}", + "approvedBadge": "Рекомендовано", + "clampBorder": "Граница", + "clampClamp": "Зажатие", + "clampRepeat": "Повтор", + "closeSidebar": "Закрыть боковую панель", + "compatibilityQuickCheck": "Быстрая проверка совместимости", + "compatibilityQuickCheckFinished": "Проверено обоев: {total}, могут работать некорректно: {failed}, ограниченная поддержка: {limited}", + "dynamicBadge": "Динамические", + "emptyAll": "В исходном каталоге не найдено ни одних обоев", + "emptyFiltered": "Не найдено обоев, соответствующих текущим фильтрам", "errorBannerTitle": "Обнаружена ошибка выполнения", - "errorShowDetails": "Показать подробности", - "errorHideDetails": "Скрыть подробности", + "errorCopied": "Подробности ошибки скопированы", + "errorCopy": "Скопировать ошибку", "errorDismiss": "Закрыть", - "refreshWallpapers": "Обновить обои", - "start": "Запустить", - "stop": "Остановить", - "confirm": "Подтвердить", - "cancel": "Отмена", - "compatibilityQuickCheck": "Быстрая проверка совместимости", - "compatibilityQuickCheckRunning": "Проверка совместимости обоев...", - "compatibilityQuickCheckConfirm": "Просканировать все обои на совместимость дополнительных свойств? Это может занять некоторое время.", - "compatibilityQuickCheckFinished": "Проверено обоев: {total}, могут работать некорректно: {failed}", + "errorHideDetails": "Скрыть подробности", + "errorShowDetails": "Показать подробности", + "filterAll": "Все", + "filterButtonSummary": "Фильтр: {type}", + "filterRes4k": "Разрешение: 4K+", + "filterResAll": "Разрешение: все", + "filterResUnknown": "Разрешение: неизвестно", + "filterTypeAll": "Все", + "filterTypeApplication": "Приложение", + "filterTypeScene": "Сцена", + "filterTypeVideo": "Видео", + "filterTypeWeb": "Веб", "folderInvalid": "Каталог источника обоев недействителен или недоступен.", - "scanning": "Сканирование обоев...", - "searchPlaceholder": "Поиск обоев по названию или ID", - "searchClear": "Очистить поиск", - "filterButtonSummary": "Фильтр · {type}", - "sortButtonSummary": "Сортировка · {direction} {sort}", - "applyAllDisplays": "Применить ко всем дисплеям", - "closePanel": "Закрыть панель", - "confirmApply": "Применить обои", - "syncWallpaperColors": "Синхронизировать цвета", - "wallpaperScaling": "Масштабирование", - "wallpaperClamp": "Режим зажатия", - "wallpaperVolume": "Громкость", - "wallpaperMuted": "Без звука", - "wallpaperAudioReactive": "Аудиореактивные эффекты", - "wallpaperDisableMouse": "Отключить мышь", - "wallpaperDisableParallax": "Отключить параллакс", - "sectionProperties": "Дополнительные свойства", - "loadingProperties": "Загрузка свойств обоев...", - "propertiesLoadFailed": "Не удалось загрузить свойства обоев. Возможно, формат свойств этих обоев пока не поддерживается, либо сами обои не могут быть корректно загружены и запущены", - "propertiesFailedBadge": "Может не работать", - "noEditableProperties": "У этих обоев пока нет поддерживаемых редактируемых дополнительных свойств", - "propertiesNotice": "Здесь показаны только свойства, которые этот плагин умеет редактировать напрямую", - "infoType": "Тип", "infoId": "ID", + "infoDescription": "Описание", "infoResolution": "Разрешение", "infoSize": "Размер", + "infoType": "Тип", + "installHint": "Пример установки: yay -S linux-wallpaperengine-git", + "loadingProperties": "Загрузка свойств обоев...", + "musicBadge": "Музыка", + "nextPage": "Вперёд", + "noEditableProperties": "У этих обоев пока нет поддерживаемых редактируемых дополнительных свойств", + "pageRange": "{start}-{end} / {total}", + "pageSummary": "Страница {current} / {total}", + "prevPage": "Назад", + "propertiesFailedBadge": "Может не работать", + "propertiesLimitedBadge": "Ограничено", + "propertiesLoadFailed": "Не удалось загрузить свойства обоев. Возможно, формат свойств этих обоев пока не поддерживается, либо сами обои не могут быть корректно загружены и запущены", + "propertiesNotice": "Здесь показаны только свойства, которые этот плагин умеет редактировать напрямую", + "propertyLabelThemeColor": "Цвет темы", + "reactiveBadge": "Аудиореакция", + "refreshWallpapers": "Обновить обои", + "resolutionUnknown": "Неизвестно", + "scalingDefault": "По умолчанию", "scalingFill": "Заполнение", "scalingFit": "По размеру", "scalingStretch": "Растянуть", - "scalingDefault": "По умолчанию", - "clampClamp": "Зажатие", - "clampBorder": "Граница", - "clampRepeat": "Повтор", - "propertyLabelThemeColor": "Цвет темы", - "targetAllDisplays": "Цель: все дисплеи", - "targetSingleDisplay": "Цель: {screen}", - "applySingleDisplay": "Применить к {screen}", - "applyTarget": "Выбрать цель применения", - "filterAll": "Все", - "filterTypeAll": "Все", - "filterTypeScene": "Сцена", - "filterTypeVideo": "Видео", - "filterTypeWeb": "Веб", - "filterTypeApplication": "Приложение", - "filterResAll": "Разрешение: все", - "filterRes4k": "Разрешение: 4K", - "filterRes8k": "Разрешение: 8K", - "filterResUnknown": "Разрешение: неизвестно", - "filterStatic": "Статические", - "filterDynamic": "Динамические", - "sortName": "Имя", + "scanning": "Сканирование обоев...", + "searchClear": "Очистить поиск", + "searchPlaceholder": "Поиск обоев по названию или ID", + "showMore": "Показать больше", + "showLess": "Свернуть", + "sectionAudio": "Отображение и аудио", + "sectionFeatures": "Функции", + "sectionProperties": "Дополнительные свойства", + "sortAscendingToggleWithDirection": "{direction} переключить возр./убыв.", + "sortButtonSummary": "Сортировка · {direction} {sort}", "sortDateAdded": "Дата добавления", + "sortName": "Имя", "sortSize": "Размер", "sortRecent": "Недавние", - "sortAscendingToggle": "Переключить возр./убыв.", - "sortAscendingToggleWithDirection": "{direction} переключить возр./убыв.", - "sortId": "ID", + "start": "Запустить", + "staticBadge": "Статические", + "stop": "Остановить", + "syncWallpaperColors": "Синхронизировать цвета", + "targetAllDisplays": "Цель: все дисплеи", + "targetSingleDisplay": "Цель: {screen}", + "title": "Wallpaper-Engine Выбор обоев", + "typeApplication": "Приложение", "typeScene": "Сцена", "typeVideo": "Видео", "typeWeb": "Веб", - "typeApplication": "Приложение", - "resolutionUnknown": "Неизвестно", - "dynamicBadge": "Динамические", - "staticBadge": "Статические", - "prevPage": "Назад", - "nextPage": "Вперёд", - "pageSummary": "Страница {current} / {total}", - "pageRange": "{start}-{end} / {total}", - "count": "Количество обоев: {count}", - "emptyAll": "В исходном каталоге не найдено ни одних обоев", - "emptyFiltered": "Не найдено обоев, соответствующих текущим фильтрам" + "wallpaperAudioReactive": "Аудиореактивные эффекты", + "wallpaperClamp": "Режим зажатия", + "wallpaperDisableMouse": "Отключить мышь", + "wallpaperDisableParallax": "Отключить параллакс", + "wallpaperMuted": "Без звука", + "wallpaperScaling": "Масштабирование", + "wallpaperVolume": "Громкость" }, "settings": { - "category": { - "interfaceTitle": "Интерфейс", - "performanceTitle": "Производительность", - "compatibilityTitle": "Ресурсы", - "audioTitle": "Аудио", - "displayTitle": "Отображение" - }, - "defaults": { - "title": "Параметры применения по умолчанию", - "description": "Эти параметры используются по умолчанию при применении обоев, если не заданы отдельные настройки для конкретных обоев." - }, - "iconColor": { - "label": "Цвет значка", - "description": "Цвет значка виджета на панели" - }, - "enableExtraPropertiesEditor": { - "label": "Включить редактор дополнительных свойств", - "description": "Показывать дополнительные свойства обоев в боковой панели, если они доступны" + "autoApplyOnStartup": { + "description": "Если хотя бы для одного экрана настроены обои, автоматически запускать движок при старте", + "label": "Автоприменение при запуске" }, "cache": { + "clear": "Очистить кэш цветов", "currentSize": "Размер кэша цветовых изображений: {size}", "refresh": "Обновить размер кэша", - "clear": "Очистить кэш цветов", "sizeUnknown": "Неизвестно" }, - "units": { - "fps": " FPS", - "percent": " %", - "minutes": " мин" + "defaults": { + "title": "Значения по умолчанию", + "description": "Следующие настройки являются значениями по умолчанию только для новых обоев. Они не влияют на уже настроенные обои." }, - "wallpapersFolder": { - "label": "Каталог источника обоев", - "description": "Каталог, содержащий папки проектов обоев", - "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", - "scan": "Автоматически определить каталог мастерской Steam" + "category": { + "audioTitle": "Аудио", + "compatibilityTitle": "Ресурсы", + "displayTitle": "Отображение", + "interfaceTitle": "Интерфейс", + "performanceTitle": "Производительность" }, - "assetsDir": { - "label": "Каталог ресурсов Wallpaper Engine", - "description": "Необязательно: путь к assets, используемый linux-wallpaperengine", - "placeholder": "~/.local/share/wallpaperengine/assets" + "resourcesIntro": "Управляйте сканированием источника обоев и кэшированными изображениями для извлечения цветов.", + "defaultAudioReactiveEffects": { + "description": "По умолчанию сохранять обработку аудио для обоев, поддерживающих эту функцию", + "label": "Эффекты аудиореакции" }, - "wallpaperScanCacheMinutes": { - "label": "Время кэша сканирования обоев", - "description": "При открытии панели повторно использовать кэшированный список обоев, если он не старше указанного количества минут" + "defaultClamp": { + "description": "Способ обработки границ, используемый по умолчанию при рендеринге обоев", + "label": "Режим зажатия по умолчанию" }, - "defaultScaling": { - "label": "Режим масштабирования", - "description": "Режим масштабирования по умолчанию, если пользовательское значение не задано" + "defaultDisableMouse": { + "description": "Запускать обои с отключённым взаимодействием мыши по умолчанию", + "label": "Отключить взаимодействие мышью" }, - "defaultClamp": { - "label": "Режим зажатия", - "description": "Способ обработки границ, используемый по умолчанию при рендеринге обоев" + "defaultDisableParallax": { + "description": "Запускать обои с отключённым эффектом параллакса по умолчанию", + "label": "Отключить эффект параллакса" }, "defaultFps": { - "label": "Ограничение FPS", "description": "Ограничение частоты кадров для обоев по умолчанию", - "placeholder": "30" + "label": "FPS по умолчанию" + }, + "defaultFullscreenPauseOnlyActive": { + "description": "(Только Wayland) Приостанавливать только когда активно полноэкранное окно", + "label": "Приостанавливать только для активного полноэкранного окна" + }, + "defaultMuted": { + "description": "Запускать обои в беззвучном режиме по умолчанию", + "label": "Отключать звук обоев по умолчанию" + }, + "defaultNoAutomute": { + "description": "Если включено, звук обоев продолжит играть, даже когда другие приложения выводят аудио", + "label": "Не заглушать при звуке других приложений" }, "defaultNoFullscreenPause": { - "label": "Не приостанавливать в полноэкранном режиме", - "description": "Не приостанавливать обои во время работы полноэкранных приложений" + "description": "Не приостанавливать обои во время работы полноэкранных приложений", + "label": "Не приостанавливать в полноэкранном режиме" }, - "defaultFullscreenPauseOnlyActive": { - "label": "Приостанавливать только для активного полноэкранного окна", - "description": "(Только Wayland) Приостанавливать только когда активно полноэкранное окно" + "defaultScaling": { + "description": "Режим масштабирования по умолчанию, если пользовательское значение не задано", + "label": "Режим масштабирования по умолчанию" }, "defaultVolume": { - "label": "Громкость", - "description": "Громкость, используемая при запуске обоев без беззвучного режима" + "description": "Громкость, используемая при запуске обоев (когда не включено беззвучие)", + "label": "Громкость обоев по умолчанию" }, - "defaultMuted": { - "label": "Запускать без звука", - "description": "Запускать обои в беззвучном режиме по умолчанию" - }, - "defaultAudioReactiveEffects": { - "label": "Эффекты аудиореакции", - "description": "По умолчанию оставлять включённой обработку аудио в обоях, если она поддерживается" + "enableExtraPropertiesEditor": { + "description": "Показывать дополнительные свойства обоев в боковой панели, если они доступны", + "label": "Включить редактор дополнительных свойств" + }, + "showSidebarDescription": { + "description": "Показывать описание обоев в карточке предпросмотра на боковой панели", + "label": "Показывать описание в боковой панели" + }, + "badges": { + "title": "Порядок значков на карточке обоев", + "description": "Меняйте порядок перетаскиванием или стрелками и отдельно включайте каждый значок.", + "moveUp": "Переместить вверх", + "moveDown": "Переместить вниз", + "items": { + "type": "Тип", + "dynamic": "Динамичные/Статичные", + "music": "Музыка", + "reactive": "Аудиореакция", + "approved": "Рекомендовано", + "resolution": "Разрешение", + "compatibility": "Совместимость" + } }, - "defaultNoAutomute": { - "label": "Не заглушать при звуке других приложений", - "description": "Если включено, звук обоев продолжит играть, даже когда другие приложения выводят аудио" + "iconColor": { + "description": "Цвет значка виджета на панели", + "label": "Цвет значка" }, - "defaultDisableMouse": { - "label": "Отключить взаимодействие мышью", - "description": "По умолчанию запускать обои с отключённым вводом мыши" + "units": { + "fps": " FPS", + "minutes": " мин", + "percent": " %" }, - "defaultDisableParallax": { - "label": "Отключить эффект параллакса", - "description": "По умолчанию запускать обои с отключённым эффектом параллакса" + "wallpaperScanCacheMinutes": { + "description": "При открытии панели повторно использовать кэшированный список обоев, если он не старше указанного количества минут", + "label": "Время кэша сканирования обоев" }, - "autoApplyOnStartup": { - "label": "Автоприменение при запуске", - "description": "Если хотя бы для одного экрана настроены обои, автоматически запускать движок при старте" + "wallpapersFolder": { + "description": "Каталог, содержащий папки проектов обоев", + "label": "Каталог источника обоев", + "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", + "scan": "Автоматически определить каталог мастерской Steam" + } + }, + "toast": { + "refreshSkippedNoFolder": "Каталог источника обоев пуст, поэтому список обоев нельзя обновить", + "refreshedWallpapers": "Список обоев обновлён: {count}", + "reloadSkippedNoWallpaper": "Обои не настроены, поэтому перезагружать нечего", + "reloaded": "Движок обоев перезагружен", + "stopped": "Движок обоев остановлен", + "wallpaperColorsApplied": "Цвета обоев применены к Noctalia", + "wallpaperColorsCached": "Цвета обоев сохранены для этого дисплея", + "wallpaperColorsEngineUnavailable": "Для генерации цветов обоев требуется доступный linux-wallpaperengine", + "wallpaperColorsFailed": "Не удалось извлечь цвета из выбранных обоев", + "wallpaperColorsNoSelection": "Сначала выберите обои, а затем применяйте их цвета" + }, + "widget": { + "tooltip": { + "checking": "Проверка linux-wallpaperengine", + "ready": "Открыть панель движка обоев", + "running": "Движок обоев применяет настройки", + "unavailable": "linux-wallpaperengine не установлен" } } } diff --git a/linux-wallpaperengine-controller/i18n/zh-CN.json b/linux-wallpaperengine-controller/i18n/zh-CN.json index 568690102..2adf1a787 100644 --- a/linux-wallpaperengine-controller/i18n/zh-CN.json +++ b/linux-wallpaperengine-controller/i18n/zh-CN.json @@ -1,238 +1,235 @@ { - "widget": { - "tooltip": { - "checking": "正在检查 linux-wallpaperengine", - "unavailable": "未安装 linux-wallpaperengine", - "running": "壁纸引擎正在应用配置", - "ready": "打开壁纸引擎面板" - } - }, - "menu": { - "refreshWallpapers": "刷新壁纸列表", - "start": "启动壁纸引擎", - "stop": "停止壁纸引擎", - "settings": "插件设置" - }, - "toast": { - "reloaded": "壁纸引擎已重新加载", - "stopped": "壁纸引擎已停止", - "reloadSkippedNoWallpaper": "当前未配置壁纸,因此无需重新加载", - "refreshingWallpapers": "正在刷新壁纸列表...", - "refreshedWallpapers": "壁纸列表已刷新:共 {count} 个壁纸", - "refreshSkippedNoFolder": "壁纸源目录为空,因此无法刷新壁纸列表", - "wallpaperColorsGenerating": "正在生成用于取色的壁纸截图...", - "wallpaperColorsApplied": "已将壁纸颜色应用到 Noctalia", - "wallpaperColorsCached": "已为此显示器缓存壁纸颜色", - "wallpaperColorsFailed": "无法从所选壁纸中提取颜色", - "wallpaperColorsNoSelection": "请先选择壁纸,再应用壁纸颜色", - "wallpaperColorsEngineUnavailable": "需要可用的 linux-wallpaperengine 才能生成壁纸颜色" - }, "main": { - "status": { - "unavailable": "引擎不可用", - "ready": "就绪", - "starting": "正在启动引擎", - "stopped": "已停止", - "crashed": "引擎异常退出" - }, "error": { - "notInstalled": "在 PATH 中未找到 linux-wallpaperengine,请先检查是否正确安装。", "assetsMissing": "未找到 Wallpaper Engine 资源,请在设置中配置资源目录。", + "autoRecovered": "已自动恢复到上次可用布局", "noBackground": "未配置任何壁纸路径。", - "opengl": "OpenGL 初始化失败,请检查驱动与合成器兼容性。", - "autoRecovered": "已自动恢复到上次可用布局" + "notInstalled": "在 PATH 中未找到 linux-wallpaperengine,请先检查是否正确安装。", + "opengl": "OpenGL 初始化失败,请检查驱动与合成器兼容性。" } }, + "menu": { + "refreshWallpapers": "刷新壁纸列表", + "settings": "插件设置", + "start": "启动壁纸引擎", + "stop": "停止壁纸引擎" + }, "panel": { - "title": "Wallpaper-Engine 壁纸选择器", - "statusChecking": "检查中", - "statusUnavailable": "不可用", - "statusRunning": "运行中", - "statusReady": "已配置", - "statusStopped": "已停止", - "sectionAudio": "显示与音频", - "sectionFeatures": "功能", - "installHint": "安装示例:yay -S linux-wallpaperengine-git", + "applyAllDisplays": "应用到全部显示器", + "applySingleDisplay": "应用到 {screen}", + "approvedBadge": "推荐", + "clampBorder": "边框", + "clampClamp": "钳制", + "clampRepeat": "重复", + "closeSidebar": "关闭侧栏", + "compatibilityQuickCheck": "兼容性快速检查", + "compatibilityQuickCheckFinished": "共扫描 {total} 个壁纸,其中 {failed} 个可能异常,{limited} 个支持受限", + "dynamicBadge": "动态", + "emptyAll": "源目录中未找到任何壁纸", + "emptyFiltered": "源目录中未找到符合条件的壁纸", "errorBannerTitle": "检测到运行时错误", - "errorShowDetails": "查看详情", - "errorHideDetails": "收起详情", + "errorCopied": "已复制错误详情", + "errorCopy": "复制错误", "errorDismiss": "关闭提示", - "refreshWallpapers": "刷新壁纸", - "start": "启动", - "stop": "停止", - "confirm": "确认", - "cancel": "取消", - "compatibilityQuickCheck": "兼容性快速检查", - "compatibilityQuickCheckRunning": "正在检查壁纸兼容性...", - "compatibilityQuickCheckConfirm": "是否扫描全部壁纸的额外属性兼容性?这可能需要一些时间。", - "compatibilityQuickCheckFinished": "共扫描 {total} 个壁纸,其中 {failed} 个可能异常", + "errorHideDetails": "收起详情", + "errorShowDetails": "查看详情", + "filterAll": "全部", + "filterButtonSummary": "筛选:{type}", + "filterRes4k": "分辨率:4K+", + "filterResAll": "分辨率:全部", + "filterResUnknown": "分辨率:未知", + "filterTypeAll": "全部", + "filterTypeApplication": "应用", + "filterTypeScene": "场景", + "filterTypeVideo": "视频", + "filterTypeWeb": "网页", "folderInvalid": "壁纸源目录无效或不可访问。", - "scanning": "正在扫描壁纸...", - "searchPlaceholder": "搜索壁纸,可使用名称和 ID 搜索", - "searchClear": "清空搜索", - "filterButtonSummary": "筛选 · {type}", - "sortButtonSummary": "排序 · {direction}{sort}", - "applyAllDisplays": "应用到全部显示器", - "closePanel": "关闭面板", - "confirmApply": "应用壁纸", - "syncWallpaperColors": "同步壁纸颜色", - "wallpaperScaling": "缩放", - "wallpaperClamp": "边界模式", - "wallpaperVolume": "音量", - "wallpaperMuted": "静音", - "wallpaperAudioReactive": "音频响应效果", - "wallpaperDisableMouse": "禁用鼠标交互", - "wallpaperDisableParallax": "禁用视差效果", - "sectionProperties": "额外属性", - "loadingProperties": "正在读取壁纸属性...", - "propertiesLoadFailed": "读取壁纸属性失败,可能是该壁纸的属性格式暂未支持,或该壁纸本身无法被正确加载和运行", - "propertiesFailedBadge": "可能异常", - "noEditableProperties": "此壁纸暂时没有可编辑的额外属性", - "propertiesNotice": "这里只显示当前插件可直接编辑的属性", - "infoType": "类型", "infoId": "ID", + "infoDescription": "简介", "infoResolution": "分辨率", "infoSize": "大小", + "infoType": "类型", + "installHint": "安装示例:yay -S linux-wallpaperengine-git", + "loadingProperties": "正在读取壁纸属性...", + "musicBadge": "音乐", + "nextPage": "下一页", + "noEditableProperties": "此壁纸暂时没有可编辑的额外属性", + "pageRange": "{start}-{end} / 共 {total} 项", + "pageSummary": "第 {current} / {total} 页", + "prevPage": "上一页", + "propertiesFailedBadge": "可能异常", + "propertiesLimitedBadge": "受限", + "propertiesLoadFailed": "读取壁纸属性失败,可能是该壁纸的属性格式暂未支持,或该壁纸本身无法被正确加载和运行", + "propertiesNotice": "这里只显示当前插件可直接编辑的属性", + "propertyLabelThemeColor": "主题色", + "reactiveBadge": "音频响应", + "refreshWallpapers": "刷新壁纸", + "resolutionUnknown": "未知", + "scalingDefault": "默认", "scalingFill": "填充", "scalingFit": "适应", "scalingStretch": "拉伸", - "scalingDefault": "默认", - "clampClamp": "钳制", - "clampBorder": "边框", - "clampRepeat": "重复", - "propertyLabelThemeColor": "主题色", - "targetAllDisplays": "目标:全部显示器", - "targetSingleDisplay": "目标:{screen}", - "applySingleDisplay": "应用到 {screen}", - "applyTarget": "选择应用目标", - "filterAll": "全部", - "filterTypeAll": "全部", - "filterTypeScene": "场景", - "filterTypeVideo": "视频", - "filterTypeWeb": "网页", - "filterTypeApplication": "应用", - "filterResAll": "分辨率:全部", - "filterRes4k": "分辨率:4K", - "filterRes8k": "分辨率:8K", - "filterResUnknown": "分辨率:未知", - "filterStatic": "静态", - "filterDynamic": "动态", - "sortName": "名称", + "scanning": "正在扫描壁纸...", + "searchClear": "清空搜索", + "searchPlaceholder": "搜索壁纸,可使用名称和 ID 搜索", + "showMore": "展开更多", + "showLess": "收起", + "sectionAudio": "显示与音频", + "sectionFeatures": "功能", + "sectionProperties": "额外属性", + "sortAscendingToggleWithDirection": "{direction} 切换升/降序", + "sortButtonSummary": "排序 · {direction}{sort}", "sortDateAdded": "添加时间", + "sortName": "名称", "sortSize": "大小", "sortRecent": "最近", - "sortAscendingToggle": "切换升/降序", - "sortAscendingToggleWithDirection": "{direction} 切换升/降序", - "sortId": "ID", + "start": "启动", + "staticBadge": "静态", + "stop": "停止", + "syncWallpaperColors": "同步壁纸颜色", + "targetAllDisplays": "目标:全部显示器", + "targetSingleDisplay": "目标:{screen}", + "title": "Wallpaper-Engine 壁纸选择器", + "typeApplication": "应用", "typeScene": "场景", "typeVideo": "视频", "typeWeb": "网页", - "typeApplication": "应用", - "resolutionUnknown": "未知", - "dynamicBadge": "动态", - "staticBadge": "静态", - "prevPage": "上一页", - "nextPage": "下一页", - "pageSummary": "第 {current} / {total} 页", - "pageRange": "{start}-{end} / 共 {total} 项", - "count": "壁纸数量:{count}", - "emptyAll": "源目录中未找到任何壁纸", - "emptyFiltered": "源目录中未找到符合条件的壁纸" + "wallpaperAudioReactive": "音频响应效果", + "wallpaperClamp": "边界模式", + "wallpaperDisableMouse": "禁用鼠标交互", + "wallpaperDisableParallax": "禁用视差效果", + "wallpaperMuted": "静音", + "wallpaperScaling": "缩放", + "wallpaperVolume": "音量" }, "settings": { - "category": { - "interfaceTitle": "界面", - "performanceTitle": "性能", - "compatibilityTitle": "资源", - "audioTitle": "音频", - "displayTitle": "显示" - }, - "defaults": { - "title": "默认应用选项", - "description": "当你应用壁纸且未覆盖其单独设置时,这些选项会作为默认值使用。" - }, - "iconColor": { - "label": "图标颜色", - "description": "用于栏组件图标的颜色" - }, - "enableExtraPropertiesEditor": { - "label": "启用额外属性编辑器", - "description": "在可用时于面板侧栏显示壁纸专属的额外属性" + "autoApplyOnStartup": { + "description": "若至少一个屏幕已配置壁纸,启动时自动运行引擎", + "label": "启动时自动应用" }, - "cache": { - "currentSize": "当前配色图片缓存大小:{size}", - "refresh": "刷新配色缓存大小", - "clear": "清除配色图片缓存", +"cache": { + "clear": "清除颜色截图缓存", + "currentSize": "当前颜色截图缓存大小:{size}", + "refresh": "刷新缓存大小", "sizeUnknown": "未知" }, - "units": { - "fps": " FPS", - "percent": " %", - "minutes": " 分钟" + "defaults": { + "title": "默认值", + "description": "以下设置仅作为新建壁纸时的默认值,不影响已配置的壁纸" }, - "wallpapersFolder": { - "label": "壁纸源目录", - "description": "包含壁纸项目文件夹的目录", - "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", - "scan": "自动检测 Steam 创意工坊目录" + "category": { + "audioTitle": "音频", + "compatibilityTitle": "资源", + "displayTitle": "显示", + "interfaceTitle": "界面", + "performanceTitle": "性能" }, - "assetsDir": { - "label": "Wallpaper Engine 资源目录", - "description": "可选:linux-wallpaperengine 使用的 assets 路径", - "placeholder": "~/.local/share/wallpaperengine/assets" + "resourcesIntro": "管理壁纸源扫描,以及用于取色的缓存图片。", + "defaultAudioReactiveEffects": { + "description": "支持该功能的壁纸默认保持音频处理启用状态", + "label": "音频响应效果" }, - "wallpaperScanCacheMinutes": { - "label": "壁纸扫描缓存时长", - "description": "打开面板时,若未超过该分钟数则复用缓存的壁纸列表而不重新扫描" + "defaultClamp": { + "description": "渲染壁纸时默认使用的边界处理方式", + "label": "默认边界模式" }, - "defaultScaling": { - "label": "缩放模式", - "description": "未设置自定义值时的默认缩放模式" + "defaultDisableMouse": { + "description": "默认以禁用鼠标输入的状态启动壁纸", + "label": "禁用鼠标交互" }, - "defaultClamp": { - "label": "边界模式", - "description": "渲染壁纸时默认使用的边界处理方式" + "defaultDisableParallax": { + "description": "默认以禁用视差效果的状态启动壁纸", + "label": "禁用视差效果" }, "defaultFps": { - "label": "FPS 限制", "description": "壁纸默认使用的帧率上限", - "placeholder": "30" + "label": "默认 FPS" + }, + "defaultFullscreenPauseOnlyActive": { + "description": "(仅限 Wayland)仅在全屏窗口激活时暂停", + "label": "仅激活全屏时暂停" + }, + "defaultMuted": { + "description": "默认以静音模式启动壁纸", + "label": "默认静音壁纸" + }, + "defaultNoAutomute": { + "description": "开启后,其他应用输出声音时壁纸也会继续播放音频", + "label": "其他应用播放音频时不静音" }, "defaultNoFullscreenPause": { - "label": "防止全屏时暂停", - "description": "防止在全屏应用运行时暂停" + "description": "防止在全屏应用运行时暂停", + "label": "防止全屏时暂停" }, - "defaultFullscreenPauseOnlyActive": { - "label": "仅激活全屏时暂停", - "description": "(仅限 Wayland)仅在全屏窗口激活时暂停" + "defaultScaling": { + "description": "未设置自定义值时的默认缩放模式", + "label": "默认缩放模式" }, "defaultVolume": { - "label": "音量", - "description": "壁纸在未启用静音时使用的播放音量" + "description": "壁纸启动时(未启用静音时)使用的音量", + "label": "默认壁纸音量" }, - "defaultMuted": { - "label": "启动时静音", - "description": "默认以静音模式启动壁纸" - }, - "defaultAudioReactiveEffects": { - "label": "音频响应效果", - "description": "在壁纸支持时,默认保持启用音频处理效果" + "enableExtraPropertiesEditor": { + "description": "在可用时于面板侧栏显示壁纸专属的额外属性", + "label": "启用额外属性编辑器" + }, + "showSidebarDescription": { + "description": "在侧栏预览卡片中显示壁纸简介", + "label": "显示侧栏简介" + }, + "badges": { + "title": "壁纸卡片 Badge 显示顺序", + "description": "可拖拽或用箭头调整顺序,并单独开关每个 Badge。", + "moveUp": "上移", + "moveDown": "下移", + "items": { + "type": "类型", + "dynamic": "动态/静态", + "music": "音乐", + "reactive": "音频响应", + "approved": "推荐", + "resolution": "分辨率", + "compatibility": "兼容性" + } }, - "defaultNoAutomute": { - "label": "其他应用播放音频时不静音", - "description": "开启后,其他应用输出声音时壁纸也会继续播放音频" + "iconColor": { + "description": "用于栏组件图标的颜色", + "label": "图标颜色" }, - "defaultDisableMouse": { - "label": "禁用鼠标交互", - "description": "默认以禁用鼠标输入的方式启动壁纸" + "units": { + "fps": " FPS", + "minutes": " 分钟", + "percent": " %" }, - "defaultDisableParallax": { - "label": "禁用视差效果", - "description": "默认以关闭视差效果的方式启动壁纸" + "wallpaperScanCacheMinutes": { + "description": "打开面板时,若未超过该分钟数则复用缓存的壁纸列表而不重新扫描", + "label": "壁纸扫描缓存时长" }, - "autoApplyOnStartup": { - "label": "启动时自动应用", - "description": "若至少一个屏幕已配置壁纸,启动时自动运行引擎" + "wallpapersFolder": { + "description": "包含壁纸项目文件夹的目录", + "label": "壁纸源目录", + "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", + "scan": "自动检测 Steam 创意工坊目录" + } + }, + "toast": { + "refreshSkippedNoFolder": "壁纸源目录为空,因此无法刷新壁纸列表", + "refreshedWallpapers": "壁纸列表已刷新:共 {count} 个壁纸", + "reloadSkippedNoWallpaper": "当前未配置壁纸,因此无需重新加载", + "reloaded": "壁纸引擎已重新加载", + "stopped": "壁纸引擎已停止", + "wallpaperColorsApplied": "已将壁纸颜色应用到 Noctalia", + "wallpaperColorsCached": "已为此显示器缓存壁纸颜色", + "wallpaperColorsEngineUnavailable": "需要可用的 linux-wallpaperengine 才能生成壁纸颜色", + "wallpaperColorsFailed": "无法从所选壁纸中提取颜色", + "wallpaperColorsNoSelection": "请先选择壁纸,再应用壁纸颜色" + }, + "widget": { + "tooltip": { + "checking": "正在检查 linux-wallpaperengine", + "ready": "打开壁纸引擎面板", + "running": "壁纸引擎正在应用配置", + "unavailable": "未安装 linux-wallpaperengine" } } } diff --git a/linux-wallpaperengine-controller/i18n/zh-TW.json b/linux-wallpaperengine-controller/i18n/zh-TW.json index 9b37eee6c..6af1477ae 100644 --- a/linux-wallpaperengine-controller/i18n/zh-TW.json +++ b/linux-wallpaperengine-controller/i18n/zh-TW.json @@ -1,238 +1,235 @@ { - "widget": { - "tooltip": { - "checking": "正在檢查 linux-wallpaperengine", - "unavailable": "未安裝 linux-wallpaperengine", - "running": "壁紙引擎正在套用設定", - "ready": "開啟壁紙引擎面板" - } - }, - "menu": { - "refreshWallpapers": "刷新壁紙列表", - "start": "啟動壁紙引擎", - "stop": "停止壁紙引擎", - "settings": "外掛設定" - }, - "toast": { - "reloaded": "壁紙引擎已重新載入", - "stopped": "壁紙引擎已停止", - "reloadSkippedNoWallpaper": "目前未設定壁紙,因此無需重新載入", - "refreshingWallpapers": "正在刷新壁紙列表...", - "refreshedWallpapers": "壁紙列表已刷新:共 {count} 個壁紙", - "refreshSkippedNoFolder": "壁紙來源目錄為空,因此無法刷新壁紙列表", - "wallpaperColorsGenerating": "正在產生用於取色的壁紙截圖...", - "wallpaperColorsApplied": "已將壁紙顏色套用到 Noctalia", - "wallpaperColorsCached": "已為此顯示器快取壁紙顏色", - "wallpaperColorsFailed": "無法從所選壁紙中擷取顏色", - "wallpaperColorsNoSelection": "請先選擇壁紙,再套用壁紙顏色", - "wallpaperColorsEngineUnavailable": "需要可用的 linux-wallpaperengine 才能產生壁紙顏色" - }, "main": { - "status": { - "unavailable": "引擎不可用", - "ready": "就緒", - "starting": "正在啟動引擎", - "stopped": "已停止", - "crashed": "引擎異常退出" - }, "error": { - "notInstalled": "在 PATH 中找不到 linux-wallpaperengine,請先確認是否正確安裝。", "assetsMissing": "找不到 Wallpaper Engine 資源,請在設定中設定資源目錄。", + "autoRecovered": "已自動恢復到上次可用佈局", "noBackground": "未設定任何壁紙路徑。", - "opengl": "OpenGL 初始化失敗,請檢查驅動與合成器相容性。", - "autoRecovered": "已自動恢復到上次可用佈局" + "notInstalled": "在 PATH 中找不到 linux-wallpaperengine,請先確認是否正確安裝。", + "opengl": "OpenGL 初始化失敗,請檢查驅動與合成器相容性。" } }, + "menu": { + "refreshWallpapers": "刷新壁紙列表", + "settings": "外掛設定", + "start": "啟動壁紙引擎", + "stop": "停止壁紙引擎" + }, "panel": { - "title": "Wallpaper-Engine 壁紙選擇器", - "statusChecking": "檢查中", - "statusUnavailable": "不可用", - "statusRunning": "執行中", - "statusReady": "已配置", - "statusStopped": "已停止", - "sectionAudio": "顯示與音訊", - "sectionFeatures": "功能", - "installHint": "安裝範例:yay -S linux-wallpaperengine-git", + "applyAllDisplays": "套用到全部顯示器", + "applySingleDisplay": "套用到 {screen}", + "approvedBadge": "推薦", + "clampBorder": "邊框", + "clampClamp": "鉗制", + "clampRepeat": "重複", + "closeSidebar": "關閉側欄", + "compatibilityQuickCheck": "相容性快速檢查", + "compatibilityQuickCheckFinished": "共掃描 {total} 個壁紙,其中 {failed} 個可能異常,{limited} 個支援受限", + "dynamicBadge": "動態", + "emptyAll": "來源目錄中找不到任何壁紙", + "emptyFiltered": "來源目錄中找不到符合條件的壁紙", "errorBannerTitle": "偵測到執行期錯誤", - "errorShowDetails": "查看詳情", - "errorHideDetails": "收起詳情", + "errorCopied": "已複製錯誤詳情", + "errorCopy": "複製錯誤", "errorDismiss": "關閉提示", - "refreshWallpapers": "刷新壁紙", - "start": "啟動", - "stop": "停止", - "confirm": "確認", - "cancel": "取消", - "compatibilityQuickCheck": "相容性快速檢查", - "compatibilityQuickCheckRunning": "正在檢查壁紙相容性...", - "compatibilityQuickCheckConfirm": "是否掃描全部壁紙的額外屬性相容性?這可能需要一些時間。", - "compatibilityQuickCheckFinished": "共掃描 {total} 個壁紙,其中 {failed} 個可能異常", + "errorHideDetails": "收起詳情", + "errorShowDetails": "查看詳情", + "filterAll": "全部", + "filterButtonSummary": "篩選:{type}", + "filterRes4k": "解析度:4K+", + "filterResAll": "解析度:全部", + "filterResUnknown": "解析度:未知", + "filterTypeAll": "全部", + "filterTypeApplication": "應用", + "filterTypeScene": "場景", + "filterTypeVideo": "影片", + "filterTypeWeb": "網頁", "folderInvalid": "壁紙來源目錄無效或不可存取。", - "scanning": "正在掃描壁紙...", - "searchPlaceholder": "搜尋壁紙,可使用名稱和 ID 搜尋", - "searchClear": "清除搜尋", - "filterButtonSummary": "篩選 · {type}", - "sortButtonSummary": "排序 · {direction}{sort}", - "applyAllDisplays": "套用到全部顯示器", - "closePanel": "關閉面板", - "confirmApply": "套用壁紙", - "syncWallpaperColors": "同步壁紙顏色", - "wallpaperScaling": "縮放", - "wallpaperClamp": "邊界模式", - "wallpaperVolume": "音量", - "wallpaperMuted": "靜音", - "wallpaperAudioReactive": "音訊響應效果", - "wallpaperDisableMouse": "停用滑鼠互動", - "wallpaperDisableParallax": "停用視差效果", - "sectionProperties": "額外屬性", - "loadingProperties": "正在讀取壁紙屬性...", - "propertiesLoadFailed": "讀取壁紙屬性失敗,可能是該壁紙的屬性格式尚未支援,或該壁紙本身無法被正確載入與執行", - "propertiesFailedBadge": "可能異常", - "noEditableProperties": "此壁紙目前沒有可編輯的額外屬性", - "propertiesNotice": "這裡只顯示目前外掛可直接編輯的屬性", - "infoType": "類型", "infoId": "ID", + "infoDescription": "簡介", "infoResolution": "解析度", "infoSize": "大小", + "infoType": "類型", + "installHint": "安裝範例:yay -S linux-wallpaperengine-git", + "loadingProperties": "正在讀取壁紙屬性...", + "musicBadge": "音樂", + "nextPage": "下一頁", + "noEditableProperties": "此壁紙目前沒有可編輯的額外屬性", + "pageRange": "{start}-{end} / 共 {total} 項", + "pageSummary": "第 {current} / {total} 頁", + "prevPage": "上一頁", + "propertiesFailedBadge": "可能異常", + "propertiesLimitedBadge": "受限", + "propertiesLoadFailed": "讀取壁紙屬性失敗,可能是該壁紙的屬性格式尚未支援,或該壁紙本身無法被正確載入與執行", + "propertiesNotice": "這裡只顯示目前外掛可直接編輯的屬性", + "propertyLabelThemeColor": "主題色", + "reactiveBadge": "音訊響應", + "refreshWallpapers": "刷新壁紙", + "resolutionUnknown": "未知", + "scalingDefault": "預設", "scalingFill": "填充", "scalingFit": "適應", "scalingStretch": "拉伸", - "scalingDefault": "預設", - "clampClamp": "鉗制", - "clampBorder": "邊框", - "clampRepeat": "重複", - "propertyLabelThemeColor": "主題色", - "targetAllDisplays": "目標:全部顯示器", - "targetSingleDisplay": "目標:{screen}", - "applySingleDisplay": "套用到 {screen}", - "applyTarget": "選擇套用目標", - "filterAll": "全部", - "filterTypeAll": "全部", - "filterTypeScene": "場景", - "filterTypeVideo": "影片", - "filterTypeWeb": "網頁", - "filterTypeApplication": "應用", - "filterResAll": "解析度:全部", - "filterRes4k": "解析度:4K", - "filterRes8k": "解析度:8K", - "filterResUnknown": "解析度:未知", - "filterStatic": "靜態", - "filterDynamic": "動態", - "sortName": "名稱", + "scanning": "正在掃描壁紙...", + "searchClear": "清除搜尋", + "searchPlaceholder": "搜尋壁紙,可使用名稱和 ID 搜尋", + "showMore": "展開更多", + "showLess": "收起", + "sectionAudio": "顯示與音訊", + "sectionFeatures": "功能", + "sectionProperties": "額外屬性", + "sortAscendingToggleWithDirection": "{direction} 切換升/降序", + "sortButtonSummary": "排序 · {direction}{sort}", "sortDateAdded": "加入時間", + "sortName": "名稱", "sortSize": "大小", "sortRecent": "最近", - "sortAscendingToggle": "切換升/降序", - "sortAscendingToggleWithDirection": "{direction} 切換升/降序", - "sortId": "ID", + "start": "啟動", + "staticBadge": "靜態", + "stop": "停止", + "syncWallpaperColors": "同步壁紙顏色", + "targetAllDisplays": "目標:全部顯示器", + "targetSingleDisplay": "目標:{screen}", + "title": "Wallpaper-Engine 壁紙選擇器", + "typeApplication": "應用", "typeScene": "場景", "typeVideo": "影片", "typeWeb": "網頁", - "typeApplication": "應用", - "resolutionUnknown": "未知", - "dynamicBadge": "動態", - "staticBadge": "靜態", - "prevPage": "上一頁", - "nextPage": "下一頁", - "pageSummary": "第 {current} / {total} 頁", - "pageRange": "{start}-{end} / 共 {total} 項", - "count": "壁紙數量:{count}", - "emptyAll": "來源目錄中找不到任何壁紙", - "emptyFiltered": "來源目錄中找不到符合條件的壁紙" + "wallpaperAudioReactive": "音訊響應效果", + "wallpaperClamp": "邊界模式", + "wallpaperDisableMouse": "停用滑鼠互動", + "wallpaperDisableParallax": "停用視差效果", + "wallpaperMuted": "靜音", + "wallpaperScaling": "縮放", + "wallpaperVolume": "音量" }, "settings": { - "category": { - "interfaceTitle": "介面", - "performanceTitle": "效能", - "compatibilityTitle": "資源", - "audioTitle": "音訊", - "displayTitle": "顯示" - }, - "defaults": { - "title": "預設套用選項", - "description": "當你套用壁紙且未覆蓋其個別設定時,這些選項會作為預設值使用。" - }, - "iconColor": { - "label": "圖示顏色", - "description": "用於欄元件圖示的顏色" - }, - "enableExtraPropertiesEditor": { - "label": "啟用額外屬性編輯器", - "description": "在可用時於面板側欄顯示壁紙專屬的額外屬性" + "autoApplyOnStartup": { + "description": "若至少一個螢幕已設定壁紙,啟動時自動執行引擎", + "label": "啟動時自動套用" }, "cache": { + "clear": "清除配色圖片快取", "currentSize": "目前配色圖片快取大小:{size}", "refresh": "刷新配色快取大小", - "clear": "清除配色圖片快取", "sizeUnknown": "未知" }, - "units": { - "fps": " FPS", - "percent": " %", - "minutes": " 分鐘" + "defaults": { + "title": "預設值", + "description": "以下設定僅作為新建壁紙時的預設值,不影響已設定的壁紙" }, - "wallpapersFolder": { - "label": "壁紙來源目錄", - "description": "包含壁紙項目資料夾的目錄", - "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", - "scan": "自動檢測 Steam 工作坊目錄" + "category": { + "audioTitle": "音訊", + "compatibilityTitle": "資源", + "displayTitle": "顯示", + "interfaceTitle": "介面", + "performanceTitle": "效能" }, - "assetsDir": { - "label": "Wallpaper Engine 資源目錄", - "description": "可選:linux-wallpaperengine 使用的 assets 路徑", - "placeholder": "~/.local/share/wallpaperengine/assets" + "resourcesIntro": "管理壁紙來源掃描,以及用於取色的快取圖片。", + "defaultAudioReactiveEffects": { + "description": "支援該功能的壁纸預設保持音訊處理啟用狀態", + "label": "音訊響應效果" }, - "wallpaperScanCacheMinutes": { - "label": "壁紙掃描快取時長", - "description": "開啟面板時,若未超過該分鐘數則重用快取的壁紙列表而不重新掃描" + "defaultClamp": { + "description": "渲染壁紙時預設使用的邊界處理方式", + "label": "預設邊界模式" }, - "defaultScaling": { - "label": "縮放模式", - "description": "未設定自訂值時使用的預設縮放模式" + "defaultDisableMouse": { + "description": "預設以停用滑鼠輸入的狀態啟動壁纸", + "label": "停用滑鼠互動" }, - "defaultClamp": { - "label": "邊界模式", - "description": "渲染壁紙時預設使用的邊界處理方式" + "defaultDisableParallax": { + "description": "預設以停用視差效果的狀態啟動壁纸", + "label": "停用視差效果" }, "defaultFps": { - "label": "FPS 限制", "description": "壁紙預設使用的幀率上限", - "placeholder": "30" - }, - "defaultNoFullscreenPause": { - "label": "防止全螢幕時暫停", - "description": "防止在全螢幕應用程式執行時暫停" + "label": "預設 FPS" }, "defaultFullscreenPauseOnlyActive": { - "label": "僅啟用全螢幕時暫停", - "description": "(僅限 Wayland)僅在全螢幕視窗為作用中時暫停" - }, - "defaultVolume": { - "label": "音量", - "description": "壁紙在未啟用靜音時使用的播放音量" + "description": "(僅限 Wayland)僅在全螢幕視窗為作用中時暫停", + "label": "僅啟用全螢幕時暫停" }, "defaultMuted": { - "label": "啟動時靜音", - "description": "預設以靜音模式啟動壁紙" - }, - "defaultAudioReactiveEffects": { - "label": "音訊響應效果", - "description": "在壁紙支援時,預設保持啟用音訊處理效果" + "description": "預設以靜音模式啟動壁紙", + "label": "預設靜音壁紙" }, "defaultNoAutomute": { - "label": "其他應用播放音訊時不靜音", - "description": "開啟後,其他應用輸出聲音時壁紙也會繼續播放音訊" + "description": "開啟後,其他應用輸出聲音時壁紙也會繼續播放音訊", + "label": "其他應用播放音訊時不靜音" }, - "defaultDisableMouse": { - "label": "停用滑鼠互動", - "description": "預設以停用滑鼠輸入的方式啟動壁紙" + "defaultNoFullscreenPause": { + "description": "防止在全螢幕應用程式執行時暫停", + "label": "防止全螢幕時暫停" }, - "defaultDisableParallax": { - "label": "停用視差效果", - "description": "預設以關閉視差效果的方式啟動壁紙" + "defaultScaling": { + "description": "未設定自訂值時使用的預設縮放模式", + "label": "預設縮放模式" }, - "autoApplyOnStartup": { - "label": "啟動時自動套用", - "description": "若至少一個螢幕已設定壁紙,啟動時自動執行引擎" +"defaultVolume": { + "description": "壁纸啟動時(未啟用靜音時)使用的音量", + "label": "預設壁纸音量" + }, + "enableExtraPropertiesEditor": { + "description": "在可用時於面板側欄顯示壁紙專屬的額外屬性", + "label": "啟用額外屬性編輯器" + }, + "showSidebarDescription": { + "description": "在側欄預覽卡片中顯示壁紙簡介", + "label": "顯示側欄簡介" + }, + "badges": { + "title": "壁紙卡片 Badge 顯示順序", + "description": "可拖曳或用箭頭調整順序,並個別開關每個 Badge。", + "moveUp": "上移", + "moveDown": "下移", + "items": { + "type": "類型", + "dynamic": "動態/靜態", + "music": "音樂", + "reactive": "音訊響應", + "approved": "推薦", + "resolution": "解析度", + "compatibility": "相容性" + } + }, + "iconColor": { + "description": "用於欄元件圖示的顏色", + "label": "圖示顏色" + }, + "units": { + "fps": " FPS", + "minutes": " 分鐘", + "percent": " %" + }, + "wallpaperScanCacheMinutes": { + "description": "開啟面板時,若未超過該分鐘數則重用快取的壁紙列表而不重新掃描", + "label": "壁紙掃描快取時長" + }, + "wallpapersFolder": { + "description": "包含壁纸项目文件夹的目录", + "label": "壁纸来源目录", + "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", + "scan": "自动检测 Steam 工作坊目录" + } + }, + "toast": { + "refreshSkippedNoFolder": "壁紙來源目錄為空,因此無法刷新壁紙列表", + "refreshedWallpapers": "壁紙列表已刷新:共 {count} 個壁紙", + "reloadSkippedNoWallpaper": "目前未設定壁紙,因此無需重新載入", + "reloaded": "壁紙引擎已重新載入", + "stopped": "壁紙引擎已停止", + "wallpaperColorsApplied": "已將壁紙顏色套用到 Noctalia", + "wallpaperColorsCached": "已為此顯示器快取壁紙顏色", + "wallpaperColorsEngineUnavailable": "需要可用的 linux-wallpaperengine 才能產生壁紙顏色", + "wallpaperColorsFailed": "無法從所選壁紙中擷取顏色", + "wallpaperColorsNoSelection": "請先選擇壁紙,再套用壁紙顏色" + }, + "widget": { + "tooltip": { + "checking": "正在檢查 linux-wallpaperengine", + "ready": "開啟壁紙引擎面板", + "running": "壁紙引擎正在套用設定", + "unavailable": "未安裝 linux-wallpaperengine" } } } diff --git a/linux-wallpaperengine-controller/manifest.json b/linux-wallpaperengine-controller/manifest.json index 74eef734b..d9bc542e1 100644 --- a/linux-wallpaperengine-controller/manifest.json +++ b/linux-wallpaperengine-controller/manifest.json @@ -1,7 +1,7 @@ { "id": "linux-wallpaperengine-controller", "name": "Linux WallpaperEngine Controller", - "version": "1.2.1", + "version": "1.3.0", "minNoctaliaVersion": "4.6.6", "author": "PaloMiku", "license": "MIT", @@ -24,7 +24,6 @@ "metadata": { "defaultSettings": { "wallpapersFolder": "~/.local/share/Steam/steamapps/workshop/content/431960", - "assetsDir": "", "iconColor": "none", "enableExtraPropertiesEditor": true, "defaultScaling": "fill", @@ -40,8 +39,28 @@ "wallpaperColorScreenshots": {}, "defaultNoFullscreenPause": false, "defaultFullscreenPauseOnlyActive": false, + "showSidebarDescription": false, + "badgeOrder": [ + "type", + "dynamic", + "music", + "reactive", + "approved", + "resolution", + "compatibility" + ], + "badgeEnabled": { + "type": true, + "dynamic": true, + "music": true, + "reactive": true, + "approved": true, + "resolution": true, + "compatibility": true + }, "autoApplyOnStartup": true, - "wallpaperScanCacheMinutes": 5, + "panelPageSize": 24, + "wallpaperScanCacheMinutes": 10, "panelLastSelectedPath": "", "screens": {}, "wallpaperProperties": {}, diff --git a/linux-wallpaperengine-controller/preview.png b/linux-wallpaperengine-controller/preview.png index 640dbb7a8..c4cc2934c 100644 Binary files a/linux-wallpaperengine-controller/preview.png and b/linux-wallpaperengine-controller/preview.png differ diff --git a/linux-wallpaperengine-controller/scripts/scan-properties-compatibility.sh b/linux-wallpaperengine-controller/scripts/scan-properties-compatibility.sh index 8bc00fda3..8d8a8a6ec 100644 --- a/linux-wallpaperengine-controller/scripts/scan-properties-compatibility.sh +++ b/linux-wallpaperengine-controller/scripts/scan-properties-compatibility.sh @@ -5,16 +5,27 @@ # 1: Wallpaper Engine workshop directory (contains wallpaper subdirectories) # Output: # Tab-separated rows: \t -# status: 0 = compatible, 1 = failed +# status: +# 0 = compatible +# 1 = failed +# 2 = limited editor support dir="$1" [ -d "$dir" ] || exit 10 find "$dir" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r wallpaper_dir; do - if linux-wallpaperengine "$wallpaper_dir" --list-properties >/dev/null 2>&1; then + if ! output="$(linux-wallpaperengine "$wallpaper_dir" --list-properties 2>/dev/null)"; then + status=1 + printf '%s\t%s\n' "$wallpaper_dir" "$status" + continue + fi + + if printf '%s\n' "$output" | grep -Eiq '^[^[:space:]].*[[:space:]]-[[:space:]](slider|boolean|combo|textinput|color)[[:space:]]*$'; then status=0 + elif printf '%s\n' "$output" | grep -Eiq '^[^[:space:]].*[[:space:]]-[[:space:]](text|scene texture|[[:alpha:] _-]+)[[:space:]]*$'; then + status=2 else - status=1 + status=0 fi printf '%s\t%s\n' "$wallpaper_dir" "$status" diff --git a/linux-wallpaperengine-controller/scripts/scan-wallpapers.sh b/linux-wallpaperengine-controller/scripts/scan-wallpapers.sh old mode 100644 new mode 100755 index 363c9f3b8..6d48a6de1 --- a/linux-wallpaperengine-controller/scripts/scan-wallpapers.sh +++ b/linux-wallpaperengine-controller/scripts/scan-wallpapers.sh @@ -1,78 +1,32 @@ #!/bin/bash # Scan wallpaper folders and extract metadata for panel listing. -# Args: -# 1: Wallpaper Engine workshop directory (contains wallpaper subdirectories) +# Arg 1: Wallpaper Engine workshop directory +# (Any further arguments are ignored, as the script no longer uses multi-mode) # Output: # Tab-separated rows: -# \t\t\t\t\t\t\t\t: +# \t\t\t\t\t\t\t\t\t\t:\t\t set -u - -if [ "$#" -lt 1 ]; then - exit 10 -fi - -extract_resolution_from_name() { - local source_name="$1" - local res="" - - res=$(printf '%s' "$source_name" | grep -oE '[0-9]{3,4}x[0-9]{3,4}' | head -n 1) - if [ -z "$res" ]; then printf '%s' "$source_name" | grep -qi '4k' && res='3840x2160' || true; fi - if [ -z "$res" ]; then printf '%s' "$source_name" | grep -qi '2k' && res='2560x1440' || true; fi - if [ -z "$res" ]; then printf '%s' "$source_name" | grep -qi '1080p' && res='1920x1080' || true; fi - if [ -z "$res" ]; then printf '%s' "$source_name" | grep -qi '720p' && res='1280x720' || true; fi - - printf '%s' "$res" -} - dir="${1:-}" +[ -n "$dir" ] || exit 10 [ -d "$dir" ] || exit 10 -find "$dir" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r d; do - id=$(basename "$d") - name="$id" - dynamic=0 - type=unknown - resolution=unknown - - if [ -f "$d/project.json" ]; then - if command -v jq >/dev/null 2>&1; then - title=$(jq -r '.title // empty' "$d/project.json" 2>/dev/null || true) - if [ -n "$title" ]; then name="$title"; fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PY_SCRIPT="$SCRIPT_DIR/scan_wallpapers.py" - dtype=$(jq -r '.type // empty' "$d/project.json" 2>/dev/null || true) - if [ -n "$dtype" ]; then type=$(printf '%s' "$dtype" | tr '[:upper:]' '[:lower:]'); fi - - case "$type" in - video|web) - dynamic=1 - ;; - esac - else - title=$(sed -n 's/^[[:space:]]*"title"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p' "$d/project.json" | head -n 1) - if [ -n "$title" ]; then name="$title"; fi - - dtype=$(sed -n 's/^[[:space:]]*"type"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p' "$d/project.json" | tail -n 1) - if [ -n "$dtype" ]; then type=$(printf '%s' "$dtype" | tr '[:upper:]' '[:lower:]'); fi - - grep -qi '"type"[[:space:]]*:[[:space:]]*"\(video\|web\)"' "$d/project.json" && dynamic=1 || true - fi - - res=$(extract_resolution_from_name "$name") - if [ -n "$res" ]; then resolution="$res"; fi - fi - - thumb="" - motion="" - for f in preview.jpg preview.png preview.jpeg screenshot.jpg screenshot.png screenshot.jpeg; do - if [ -f "$d/$f" ]; then thumb="$d/$f"; break; fi - done - for m in preview.gif preview.webm preview.mp4; do - if [ -f "$d/$m" ]; then motion="$d/$m"; dynamic=1; break; fi - done +if command -v python3 >/dev/null 2>&1; then + exec python3 "$PY_SCRIPT" "$dir" +fi - bytes=$(du -sb "$d" | awk '{print $1}') - mtime=$(stat -c %Y "$d") - printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$d" "$name" "$thumb" "$motion" "$dynamic" "$id" "$type" "$resolution" "$bytes:$mtime" +# Fallback: scan without metadata (when python3 is missing) +for d in "$dir"/*/; do + [ -d "$d" ] || continue + id_val="$(basename "$d")" + thumb="" + for f in "preview.jpg" "preview.png" "preview.jpeg" "screenshot.jpg" "screenshot.png" "screenshot.jpeg"; do + [ -f "$d/$f" ] && { thumb="$d/$f"; break; } + done + [ -z "$thumb" ] && continue + printf '%s\t%s\t%s\t\t0\t%s\tunknown\tunknown\t0\t0\t0:0\t0\t\n' "$d" "$id_val" "$thumb" "$id_val" done diff --git a/linux-wallpaperengine-controller/scripts/scan_wallpapers.py b/linux-wallpaperengine-controller/scripts/scan_wallpapers.py new file mode 100755 index 000000000..fa9f45b91 --- /dev/null +++ b/linux-wallpaperengine-controller/scripts/scan_wallpapers.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +Scan a Wallpaper Engine workshop directory and emit wallpaper metadata. + +Called by scan-wallpapers.sh (see that file for full documentation). + +Usage: + python3 scan_wallpapers.py +""" + +import sys +import os +import json +import re + + +def extract_resolution(name: str) -> str: + name_lower = name.lower() + match = re.search(r"([0-9]{3,4}x[0-9]{3,4})", name_lower) + if match: + return match.group(1) + if "4k" in name_lower: + return "3840x2160" + if "2k" in name_lower: + return "2560x1440" + if "1080p" in name_lower: + return "1920x1080" + if "720p" in name_lower: + return "1280x720" + return "unknown" + + +def main() -> None: + if len(sys.argv) < 2: + sys.exit(10) + + dir_path = sys.argv[1] + try: + items = os.listdir(dir_path) + except Exception: + sys.exit(10) + + for item in items: + d = os.path.join(dir_path, item) + if not os.path.isdir(d): + continue + + # Default metadata + id_val = item + name = id_val + dynamic = 0 + embedded_audio = 0 + audio_reactive = 0 + approved = 0 + type_val = "unknown" + resolution = "unknown" + description = "" + bytes_val = 0 + mtime = 0 + + try: + mtime = int(os.stat(d).st_mtime) + for root_dir, dirs, files in os.walk(d): + for name_ in files: + bytes_val += os.path.getsize(os.path.join(root_dir, name_)) + except Exception: + pass + + project_json_path = os.path.join(d, "project.json") + if os.path.isfile(project_json_path): + try: + with open(project_json_path, "r", encoding="utf-8") as f: + data = json.load(f) + + title = data.get("title", "") + if title: + name = str(title) + + desc = data.get("description", "") + if desc: + description = ( + str(desc) + .replace("\r", "") + .replace("\n", "\\n") + .replace("\t", " ") + ) + + t = data.get("type") + if t: + type_val = str(t).lower() + if type_val in ("video", "web"): + dynamic = 1 + + if data.get("supportsaudioprocessing"): + audio_reactive = 1 + + if data.get("approved"): + approved = 1 + + resolution = extract_resolution(name) + except Exception: + pass + + # Fast guessing if resolution is still unknown + if resolution == "unknown": + resolution = extract_resolution(name) + + thumb = "" + motion = "" + for f in ( + "preview.jpg", + "preview.png", + "preview.jpeg", + "screenshot.jpg", + "screenshot.png", + "screenshot.jpeg", + ): + p = os.path.join(d, f) + if os.path.isfile(p): + thumb = p + break + + for m in ("preview.gif", "preview.webm", "preview.mp4"): + p = os.path.join(d, m) + if os.path.isfile(p): + motion = p + dynamic = 1 + break + + scene_pkg_path = os.path.join(d, "scene.pkg") + if os.path.isfile(scene_pkg_path): + try: + with open(scene_pkg_path, "rb") as f: + data = f.read() + + # Basic embedded audio heuristic + if ( + b"sounds/" in data + or b".mp3" in data + or b".ogg" in data + or b".wav" in data + or b".flac" in data + or b'"sound"' in data + ): + embedded_audio = 1 + + # Basic audio reactive heuristic + if ( + b"registerAudioBuffers" in data + or b"g_AudioSpectrum" in data + or b"audio_response" in data + or b"AUDIOPROCESSING" in data + ): + audio_reactive = 1 + except Exception: + pass + + print( + f"{d}\t{name}\t{thumb}\t{motion}\t{dynamic}\t{id_val}\t" + f"{type_val}\t{resolution}\t{embedded_audio}\t{audio_reactive}\t" + f"{bytes_val}:{mtime}\t{approved}\t{description}" + ) + + +if __name__ == "__main__": + main() diff --git a/linux-wallpaperengine-controller/scripts/update-noctalia-wallpapers-cache.sh b/linux-wallpaperengine-controller/scripts/update-noctalia-wallpapers-cache.sh new file mode 100644 index 000000000..421f2f838 --- /dev/null +++ b/linux-wallpaperengine-controller/scripts/update-noctalia-wallpapers-cache.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Update Noctalia's global wallpaper cache entry for a monitor. +# Args: +# 1: screen name +# 2: screenshot path + +set -eu + +screen_name="${1:-}" +screenshot_path="${2:-}" +cache_file="$HOME/.cache/noctalia/wallpapers.json" + +if [ -z "$screen_name" ] || [ -z "$screenshot_path" ]; then + exit 1 +fi + +mkdir -p "$(dirname "$cache_file")" + +if [ ! -f "$cache_file" ]; then + printf '%s\n' '{"defaultWallpaper":"","usedRandomWallpapers":{},"wallpapers":{}}' > "$cache_file" +fi + +tmp_file="$(mktemp)" +cleanup() { + rm -f "$tmp_file" +} +trap cleanup EXIT + +jq --arg screen "$screen_name" --arg path "$screenshot_path" ' + .wallpapers = (.wallpapers // {}) | + .usedRandomWallpapers = (.usedRandomWallpapers // {}) | + .wallpapers[$screen] = { + "dark": $path, + "light": $path + } +' "$cache_file" > "$tmp_file" + +mv "$tmp_file" "$cache_file"