diff --git a/linux-wallpaperengine-controller/BarWidget.qml b/linux-wallpaperengine-controller/BarWidget.qml index 9d77e457..40e0c2c8 100644 --- a/linux-wallpaperengine-controller/BarWidget.qml +++ b/linux-wallpaperengine-controller/BarWidget.qml @@ -17,6 +17,11 @@ NIconButton { property int sectionWidgetsCount: 0 readonly property var mainInstance: pluginApi?.mainInstance + readonly property var cfg: pluginApi?.pluginSettings || ({}) + readonly property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + readonly property string iconColorKey: cfg.iconColor ?? defaults.iconColor ?? "none" + readonly property color resolvedIconColor: Color.resolveColorKey(iconColorKey) + readonly property bool hasCustomIconColor: iconColorKey !== "none" icon: "wallpaper-selector" tooltipDirection: BarService.getTooltipDirection(screen?.name) @@ -25,14 +30,8 @@ NIconButton { customRadius: Style.radiusL colorBg: Style.capsuleColor colorFg: { - if (!mainInstance?.engineAvailable) { - return Color.mError; - } - if (mainInstance?.isApplying) { - return Color.mPrimary; - } - if (mainInstance?.lastError && mainInstance.lastError.length > 0) { - return Color.mError; + if (root.hasCustomIconColor) { + return root.resolvedIconColor; } return Color.mOnSurface; } @@ -69,9 +68,9 @@ NIconButton { "icon": "refresh" }, { - "label": pluginApi?.tr("menu.stop"), - "action": "stop", - "icon": "player-stop" + "label": mainInstance?.engineRunning ? pluginApi?.tr("menu.stop") : pluginApi?.tr("menu.start"), + "action": mainInstance?.engineRunning ? "stop" : "start", + "icon": mainInstance?.engineRunning ? "player-stop" : "player-play" }, { "label": pluginApi?.tr("menu.settings"), @@ -85,9 +84,11 @@ NIconButton { PanelService.closeContextMenu(screen); if (action === "reload") { - mainInstance?.reload(); + mainInstance?.reload(true); } else if (action === "stop") { - mainInstance?.stopAll(); + mainInstance?.stopAll(true); + } else if (action === "start") { + mainInstance?.reload(true); } else if (action === "settings") { BarService.openPluginSettings(root.screen, pluginApi.manifest); } diff --git a/linux-wallpaperengine-controller/Main.qml b/linux-wallpaperengine-controller/Main.qml index fec8b142..dd9b0f72 100644 --- a/linux-wallpaperengine-controller/Main.qml +++ b/linux-wallpaperengine-controller/Main.qml @@ -18,6 +18,7 @@ Item { property string lastError: "" property string lastErrorDetails: "" property string statusMessage: "" + readonly property bool engineRunning: engineProcess.running || isApplying || pendingCommand.length > 0 property var pendingCommand: [] @@ -41,6 +42,10 @@ Item { pluginApi.pluginSettings.lastKnownGoodScreens = {}; } + if (pluginApi.pluginSettings.wallpaperProperties === undefined || pluginApi.pluginSettings.wallpaperProperties === null) { + pluginApi.pluginSettings.wallpaperProperties = {}; + } + if (pluginApi.pluginSettings.runtimeRecoveryPending === undefined || pluginApi.pluginSettings.runtimeRecoveryPending === null) { pluginApi.pluginSettings.runtimeRecoveryPending = false; } @@ -163,6 +168,7 @@ Item { } readonly property string defaultScaling: cfg.defaultScaling ?? defaults.defaultScaling ?? "fill" + readonly property string defaultClamp: cfg.defaultClamp ?? defaults.defaultClamp ?? "clamp" readonly property int defaultFps: cfg.defaultFps ?? defaults.defaultFps ?? 30 readonly property int defaultVolume: { @@ -175,6 +181,7 @@ Item { readonly property bool defaultMuted: cfg.defaultMuted ?? defaults.defaultMuted ?? true readonly property bool defaultAudioReactiveEffects: cfg.defaultAudioReactiveEffects ?? defaults.defaultAudioReactiveEffects ?? true + readonly property bool defaultNoAutomute: cfg.defaultNoAutomute ?? defaults.defaultNoAutomute ?? false readonly property bool defaultDisableMouse: cfg.defaultDisableMouse ?? defaults.defaultDisableMouse ?? false readonly property bool defaultDisableParallax: cfg.defaultDisableParallax ?? defaults.defaultDisableParallax ?? false readonly property bool defaultNoFullscreenPause: cfg.defaultNoFullscreenPause ?? defaults.defaultNoFullscreenPause ?? false @@ -190,16 +197,10 @@ Item { const screenConfigs = cfg.screens || ({}); const raw = screenConfigs[screenName] || ({}); - const resolvedVolume = Number(raw.volume ?? defaultVolume); - return { path: raw.path ?? "", scaling: raw.scaling ?? defaultScaling, - volume: isNaN(resolvedVolume) ? defaultVolume : Math.max(0, Math.min(100, Math.floor(resolvedVolume))), - muted: raw.muted ?? defaultMuted, - audioReactiveEffects: raw.audioReactiveEffects ?? defaultAudioReactiveEffects, - disableMouse: raw.disableMouse ?? defaultDisableMouse, - disableParallax: raw.disableParallax ?? defaultDisableParallax + clamp: raw.clamp ?? defaultClamp }; } @@ -213,10 +214,77 @@ Item { return false; } + function wallpaperIdFromPath(path) { + const raw = normalizedPath(path); + if (raw.length === 0) { + return ""; + } + + const parts = raw.split("/"); + return parts.length > 0 ? String(parts[parts.length - 1] || "") : ""; + } + + function cloneWallpaperProperties(source) { + const cloned = {}; + const raw = source || ({}); + for (const key of Object.keys(raw)) { + const value = raw[key]; + if (value !== undefined) { + cloned[key] = value; + } + } + return cloned; + } + + function setWallpaperProperties(path, properties) { + if (!pluginApi) { + return; + } + + ensureSettingsRoot(); + const wallpaperId = wallpaperIdFromPath(path); + if (wallpaperId.length === 0) { + return; + } + + pluginApi.pluginSettings.wallpaperProperties[wallpaperId] = cloneWallpaperProperties(properties); + } + + function getWallpaperProperties(path) { + const wallpaperId = wallpaperIdFromPath(path); + if (wallpaperId.length === 0) { + return {}; + } + + const raw = cfg.wallpaperProperties || ({}); + return cloneWallpaperProperties(raw[wallpaperId] || ({})); + } + function setScreenWallpaper(screenName, path) { setScreenWallpaperWithOptions(screenName, path, ({})); } + function clearLegacyScreenRuntimeOptions(screenName) { + const screenConfig = pluginApi?.pluginSettings?.screens?.[screenName]; + if (!screenConfig) { + return; + } + + delete screenConfig.clamp; + delete screenConfig.volume; + delete screenConfig.muted; + delete screenConfig.audioReactiveEffects; + delete screenConfig.noAutomute; + delete screenConfig.disableMouse; + delete screenConfig.disableParallax; + } + + function clearLegacyRuntimeOptionsForAllScreens() { + for (const screen of Quickshell.screens) { + clearLegacyScreenRuntimeOptions(screen.name); + } + } + function setScreenWallpaperWithOptions(screenName, path, options) { if (!pluginApi) { return; @@ -233,31 +301,45 @@ Item { pluginApi.pluginSettings.screens[screenName].path = path; const resolvedScaling = (options?.scaling || "").trim(); + const resolvedClamp = (options?.clamp || "").trim(); if (resolvedScaling.length > 0) { pluginApi.pluginSettings.screens[screenName].scaling = resolvedScaling; } + if (resolvedClamp.length > 0) { + pluginApi.pluginSettings.defaultClamp = resolvedClamp; + } if (options?.volume !== undefined) { const rawVolume = Number(options.volume); if (!isNaN(rawVolume)) { - pluginApi.pluginSettings.screens[screenName].volume = Math.max(0, Math.min(100, Math.floor(rawVolume))); + pluginApi.pluginSettings.defaultVolume = Math.max(0, Math.min(100, Math.floor(rawVolume))); } } if (options?.muted !== undefined) { - pluginApi.pluginSettings.screens[screenName].muted = !!options.muted; + pluginApi.pluginSettings.defaultMuted = !!options.muted; } if (options?.audioReactiveEffects !== undefined) { - pluginApi.pluginSettings.screens[screenName].audioReactiveEffects = !!options.audioReactiveEffects; + pluginApi.pluginSettings.defaultAudioReactiveEffects = !!options.audioReactiveEffects; + } + + if (options?.noAutomute !== undefined) { + pluginApi.pluginSettings.defaultNoAutomute = !!options.noAutomute; } if (options?.disableMouse !== undefined) { - pluginApi.pluginSettings.screens[screenName].disableMouse = !!options.disableMouse; + pluginApi.pluginSettings.defaultDisableMouse = !!options.disableMouse; } if (options?.disableParallax !== undefined) { - pluginApi.pluginSettings.screens[screenName].disableParallax = !!options.disableParallax; + pluginApi.pluginSettings.defaultDisableParallax = !!options.disableParallax; + } + + clearLegacyScreenRuntimeOptions(screenName); + + if (options?.customProperties !== undefined) { + setWallpaperProperties(path, options.customProperties); } pluginApi.saveSettings(); @@ -298,11 +380,13 @@ Item { ensureSettingsRoot(); const resolvedScaling = (options?.scaling || "").trim(); + const resolvedClamp = (options?.clamp || "").trim(); const resolvedVolumeRaw = Number(options?.volume); const hasResolvedVolume = !isNaN(resolvedVolumeRaw); const resolvedVolume = hasResolvedVolume ? Math.max(0, Math.min(100, Math.floor(resolvedVolumeRaw))) : 0; const hasMuted = options?.muted !== undefined; const hasAudioReactive = options?.audioReactiveEffects !== undefined; + const hasNoAutomute = options?.noAutomute !== undefined; const hasDisableMouse = options?.disableMouse !== undefined; const hasDisableParallax = options?.disableParallax !== undefined; @@ -315,23 +399,36 @@ Item { if (resolvedScaling.length > 0) { pluginApi.pluginSettings.screens[screen.name].scaling = resolvedScaling; } - if (hasResolvedVolume) { - pluginApi.pluginSettings.screens[screen.name].volume = resolvedVolume; - } - if (hasMuted) { - pluginApi.pluginSettings.screens[screen.name].muted = !!options.muted; - } - if (hasAudioReactive) { - pluginApi.pluginSettings.screens[screen.name].audioReactiveEffects = !!options.audioReactiveEffects; - } - if (hasDisableMouse) { - pluginApi.pluginSettings.screens[screen.name].disableMouse = !!options.disableMouse; - } - if (hasDisableParallax) { - pluginApi.pluginSettings.screens[screen.name].disableParallax = !!options.disableParallax; + if (options?.customProperties !== undefined) { + setWallpaperProperties(path, options.customProperties); } } + if (resolvedClamp.length > 0) { + pluginApi.pluginSettings.defaultClamp = resolvedClamp; + } + + if (hasResolvedVolume) { + pluginApi.pluginSettings.defaultVolume = resolvedVolume; + } + if (hasMuted) { + pluginApi.pluginSettings.defaultMuted = !!options.muted; + } + if (hasAudioReactive) { + pluginApi.pluginSettings.defaultAudioReactiveEffects = !!options.audioReactiveEffects; + } + if (hasNoAutomute) { + pluginApi.pluginSettings.defaultNoAutomute = !!options.noAutomute; + } + if (hasDisableMouse) { + pluginApi.pluginSettings.defaultDisableMouse = !!options.disableMouse; + } + if (hasDisableParallax) { + pluginApi.pluginSettings.defaultDisableParallax = !!options.disableParallax; + } + + clearLegacyRuntimeOptionsForAllScreens(); + pluginApi.saveSettings(); restartEngine(); } @@ -419,10 +516,12 @@ Item { function buildCommand() { const command = ["linux-wallpaperengine"]; let firstPath = ""; + const appendedWallpaperIds = {}; let runtimeOptions = { volume: defaultVolume, muted: defaultMuted, audioReactiveEffects: defaultAudioReactiveEffects, + noAutomute: defaultNoAutomute, disableMouse: defaultDisableMouse, disableParallax: defaultDisableParallax }; @@ -431,13 +530,6 @@ Item { const candidateCfg = getScreenConfig(candidate.name); const candidatePath = normalizedPath(candidateCfg.path); if (candidatePath && candidatePath.length > 0) { - runtimeOptions = { - volume: candidateCfg.volume, - muted: candidateCfg.muted, - audioReactiveEffects: candidateCfg.audioReactiveEffects, - disableMouse: candidateCfg.disableMouse, - disableParallax: candidateCfg.disableParallax - }; break; } } @@ -445,6 +537,12 @@ Item { 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 { @@ -456,6 +554,10 @@ Item { command.push("--no-audio-processing"); } + if (runtimeOptions.noAutomute) { + command.push("--noautomute"); + } + if (runtimeOptions.disableMouse) { command.push("--disable-mouse"); } @@ -489,12 +591,27 @@ Item { firstPath = path; } - command.push("--scaling"); - command.push(String(screenCfg.scaling)); 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; + } } if (firstPath.length > 0) { @@ -504,7 +621,7 @@ Item { return command; } - function stopAll() { + function stopAll(showToast = false) { Logger.i("LWEController", "Stopping engine process"); pendingCommand = []; @@ -522,6 +639,9 @@ Item { isApplying = false; statusMessage = pluginApi?.tr("main.status.stopped"); + if (showToast) { + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.stopped"), "player-stop"); + } } function startEngineWithCommand(command) { @@ -585,16 +705,22 @@ Item { startEngineWithCommand(command); } - function reload() { + function reload(showToast = false) { if (!hasAnyConfiguredWallpaper()) { lastError = ""; lastErrorDetails = ""; statusMessage = pluginApi?.tr("main.status.ready"); Logger.i("LWEController", "Reload skipped: no configured wallpaper paths"); + if (showToast) { + ToastService.showWarning(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.reloadSkippedNoWallpaper"), "alert-circle"); + } return; } restartEngine(); + if (showToast) { + ToastService.showNotice(pluginApi?.tr("panel.title"), pluginApi?.tr("toast.reloaded"), "refresh"); + } } Process { diff --git a/linux-wallpaperengine-controller/Panel.qml b/linux-wallpaperengine-controller/Panel.qml index 9ff40d2c..a049bfe4 100644 --- a/linux-wallpaperengine-controller/Panel.qml +++ b/linux-wallpaperengine-controller/Panel.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import QtMultimedia import Quickshell @@ -32,27 +33,39 @@ Item { property string selectedPath: "" property string pendingPath: "" 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 scanningWallpapers: false + property bool loadingWallpaperProperties: false + property bool scanningCompatibility: false + property bool pendingCompatibilityScan: false property bool folderAccessible: true property string searchText: "" property string selectedType: "all" + property string selectedResolution: "all" property string sortMode: "name" property bool sortAscending: true + property int currentPage: 0 + property int pageSize: 24 readonly property bool singleScreenMode: Quickshell.screens.length <= 1 property bool applyAllDisplays: !singleScreenMode && root._applyAllDisplays - property bool _applyAllDisplays: false + property bool _applyAllDisplays: true + 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 @@ -60,7 +73,50 @@ Item { property var screenModel: [] property var wallpaperItems: [] property var visibleWallpapers: [] + property var pagedWallpapers: [] + property var wallpaperPropertyLoadFailedByPath: ({}) + property var wallpaperPropertyDefinitions: [] + property var wallpaperPropertyValues: ({}) + property string wallpaperPropertyError: "" + property string wallpaperPropertyRequestPath: "" readonly property bool hasRuntimeError: !!(mainInstance?.lastError && mainInstance.lastError.length > 0) + 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: { const target = String(pendingPath || ""); if (target.length === 0) { @@ -79,6 +135,14 @@ Item { return parts.length > 0 ? parts[parts.length - 1] : ""; } + function workshopUrlForWallpaper(item) { + const wallpaperId = String(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("."); @@ -135,8 +199,498 @@ Item { return pluginApi?.tr("panel.sortName"); } + function resolutionBadgeIcon(value) { + const resolution = String(value || "").toLowerCase().trim(); + if (resolution.length === 0 || resolution === "unknown") { + return ""; + } + + const match = resolution.match(/(\d+)\s*[x×]\s*(\d+)/); + if (!match) { + return ""; + } + + const width = Number(match[1]); + const height = Number(match[2]); + if (isNaN(width) || isNaN(height)) { + return ""; + } + + const longestEdge = Math.max(width, height); + if (longestEdge >= 7680) { + return "badge-8k"; + } + if (longestEdge >= 3840) { + return "badge-4k"; + } + return ""; + } + + function resolutionBadgeLabel(value) { + const icon = resolutionBadgeIcon(value); + if (icon === "badge-8k") { + return "8K"; + } + if (icon === "badge-4k") { + return "4K"; + } + return ""; + } + + function resolutionFilterKey(value) { + const resolution = String(value || "").toLowerCase().trim(); + if (resolution.length === 0 || resolution === "unknown") { + return "unknown"; + } + + const match = resolution.match(/(\d+)\s*[x×]\s*(\d+)/); + if (!match) { + return "unknown"; + } + + const width = Number(match[1]); + const height = Number(match[2]); + if (isNaN(width) || isNaN(height)) { + return "unknown"; + } + + const longestEdge = Math.max(width, height); + if (longestEdge >= 7680) { + return "8k"; + } + if (longestEdge >= 3840) { + return "4k"; + } + return "other"; + } + + 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"); + } + + function wallpaperIdFromPath(path) { + const raw = String(path || "").trim(); + if (raw.length === 0) { + return ""; + } + const parts = raw.split("/"); + return parts.length > 0 ? String(parts[parts.length - 1] || "") : ""; + } + + function stripHtml(rawText) { + return String(rawText || "") + .replace(/<[^>]*>/g, " ") + .replace(/ ?/gi, " ") + .replace(/&/gi, "&") + .replace(/</gi, "<") + .replace(/>/gi, ">") + .replace(/\s+/g, " ") + .trim(); + } + + function cleanedPropertyLabel(rawText, fallbackKey) { + const stripped = stripHtml(rawText) + .replace(/^[\-–—•·*_#\s]+/, "") + .replace(/^[^\p{L}\p{N}]+/u, "") + .trim(); + if (stripped.length > 0) { + return normalizePropertyLabel(stripped); + } + return normalizePropertyLabel(String(fallbackKey || "")); + } + + function normalizePropertyLabel(value) { + 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, ""); + + const propertyLabelKey = { + "scheme_color": "panel.propertyLabelThemeColor" + }[normalizedKey.toLowerCase()]; + + if (propertyLabelKey) { + return pluginApi?.tr(propertyLabelKey); + } + + return normalizedKey + .split("_") + .filter(part => part.length > 0) + .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join(" "); + } + + 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 parsePropertyValue(rawValue, type) { + 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 (maxChannel > 1) { + return Qt.rgba(parts[0] / 255, parts[1] / 255, parts[2] / 255, 1); + } + return Qt.rgba(parts[0], parts[1], parts[2], 1); + } + return Qt.rgba(1, 1, 1, 1); + } + 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?.r ?? 1) * 255); + const g = Math.round((color?.g ?? 1) * 255); + const b = Math.round((color?.b ?? 1) * 255); + return String(r) + "," + String(g) + "," + String(b); + } + return String(value); + } + + function propertyValueFor(definition) { + const key = String(definition?.key || ""); + if (key.length === 0) { + return ""; + } + const raw = wallpaperPropertyValues || ({}); + if (raw[key] !== undefined) { + return raw[key]; + } + return definition.defaultValue; + } + + function comboChoicesFor(definition) { + const rawChoices = definition?.choices || []; + const normalized = []; + for (const choice of rawChoices) { + const key = String(choice?.key ?? choice?.value ?? "").trim(); + const name = String(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 ensureColorValue(value) { + if (value === undefined || value === null || value === "") { + return Qt.rgba(1, 1, 1, 1); + } + if (typeof value === "string") { + return parsePropertyValue(value, "color"); + } + if (value.r !== undefined && value.g !== undefined && value.b !== undefined) { + return Qt.rgba(value.r, value.g, value.b, value.a !== undefined ? value.a : 1); + } + return Qt.rgba(1, 1, 1, 1); + } + + 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 setPropertyValue(key, value) { + const current = wallpaperPropertyValues || ({}); + const next = Object.assign({}, current); + next[String(key)] = value; + wallpaperPropertyValues = next; + } + + 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; + } + + function loadWallpaperProperties(path) { + const wallpaperPath = String(path || "").trim(); + wallpaperPropertyDefinitions = []; + wallpaperPropertyValues = ({}); + wallpaperPropertyError = ""; + wallpaperPropertyRequestPath = wallpaperPath; + + if (!extraPropertiesEditorEnabled || wallpaperPath.length === 0 || !(mainInstance?.engineAvailable ?? false)) { + loadingWallpaperProperties = false; + return; + } + + loadingWallpaperProperties = true; + wallpaperPropertyProcess.command = ["linux-wallpaperengine", wallpaperPath, "--list-properties"]; + wallpaperPropertyProcess.running = true; + } + + function setWallpaperPropertyLoadFailed(path, failed) { + const wallpaperPath = String(path || "").trim(); + if (wallpaperPath.length === 0) { + return; + } + + const nextState = Object.assign({}, wallpaperPropertyLoadFailedByPath); + if (failed) { + nextState[wallpaperPath] = true; + } else { + delete nextState[wallpaperPath]; + } + wallpaperPropertyLoadFailedByPath = nextState; + } + + 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; + } + + function applyCompatibilityScanOutput(rawText) { + const nextState = {}; + const lines = String(rawText || "").split(/\r?\n/); + let totalCount = 0; + + for (const rawLine of lines) { + const line = String(rawLine || "").trim(); + if (line.length === 0) { + continue; + } + + const parts = line.split("\t"); + const path = String(parts[0] || "").trim(); + const failed = String(parts[1] || "0").trim() === "1"; + if (path.length === 0) { + continue; + } + + totalCount += 1; + + if (failed) { + nextState[path] = true; + } + } + + wallpaperPropertyLoadFailedByPath = nextState; + return { + totalCount: totalCount, + failedCount: Object.keys(nextState).length + }; + } + function closeDropdowns() { filterDropdownOpen = false; + resolutionDropdownOpen = false; sortDropdownOpen = false; } @@ -145,6 +699,7 @@ Item { filterDropdownX = pos.x; filterDropdownY = pos.y; filterDropdownWidth = filterButton.width; + resolutionDropdownOpen = false; sortDropdownOpen = false; filterDropdownOpen = true; } @@ -155,9 +710,20 @@ Item { sortDropdownY = pos.y; sortDropdownWidth = sortButton.width; filterDropdownOpen = false; + resolutionDropdownOpen = false; sortDropdownOpen = true; } + function openResolutionDropdown() { + const pos = resolutionButton.mapToItem(root, 0, resolutionButton.height + Style.marginXS); + resolutionDropdownX = pos.x; + resolutionDropdownY = pos.y; + resolutionDropdownWidth = resolutionButton.width; + filterDropdownOpen = false; + sortDropdownOpen = false; + resolutionDropdownOpen = true; + } + function applyFilterAction(action) { if (String(action).indexOf("type:") === 0) { selectedType = String(action).substring(5); @@ -165,6 +731,13 @@ Item { closeDropdowns(); } + function applyResolutionFilterAction(action) { + if (String(action).indexOf("res:") === 0) { + selectedResolution = String(action).substring(4); + } + closeDropdowns(); + } + function applySortAction(action) { if (action === "sort:toggleAscending") { sortAscending = !sortAscending; @@ -204,31 +777,33 @@ Item { function resetPendingToGlobalDefaults() { selectedScaling = String(defaults.defaultScaling || "fill"); - selectedVolume = Math.max(0, Math.min(100, Number(defaults.defaultVolume ?? 100))); - selectedMuted = !!(defaults.defaultMuted ?? true); - selectedAudioReactiveEffects = !!(defaults.defaultAudioReactiveEffects ?? true); - selectedDisableMouse = !!(defaults.defaultDisableMouse ?? false); - selectedDisableParallax = !!(defaults.defaultDisableParallax ?? false); + syncGlobalRuntimeOptions(); + } + + function syncGlobalRuntimeOptions() { + selectedClamp = String(cfg.defaultClamp ?? defaults.defaultClamp ?? "clamp"); + selectedVolume = Math.max(0, Math.min(100, Number(cfg.defaultVolume ?? defaults.defaultVolume ?? 100))); + selectedMuted = !!(cfg.defaultMuted ?? defaults.defaultMuted ?? true); + selectedAudioReactiveEffects = !!(cfg.defaultAudioReactiveEffects ?? defaults.defaultAudioReactiveEffects ?? true); + selectedDisableMouse = !!(cfg.defaultDisableMouse ?? defaults.defaultDisableMouse ?? false); + selectedDisableParallax = !!(cfg.defaultDisableParallax ?? defaults.defaultDisableParallax ?? false); } function syncSelectionOptionsFromScreen() { - if (root.singleScreenMode) { - resetPendingToGlobalDefaults(); - return; + syncGlobalRuntimeOptions(); + + const fallbackScreenName = root.singleScreenMode ? (Quickshell.screens[0]?.name || selectedScreenName) : selectedScreenName; + if (root.singleScreenMode && selectedScreenName.length === 0 && fallbackScreenName.length > 0) { + selectedScreenName = fallbackScreenName; } - const screenCfg = mainInstance?.getScreenConfig(selectedScreenName); + const screenCfg = mainInstance?.getScreenConfig(fallbackScreenName); if (!screenCfg) { - resetPendingToGlobalDefaults(); + selectedScaling = String(defaults.defaultScaling || "fill"); return; } selectedScaling = String(screenCfg.scaling || defaults.defaultScaling || "fill"); - selectedVolume = Math.max(0, Math.min(100, Number(screenCfg.volume ?? defaults.defaultVolume ?? 100))); - selectedMuted = !!(screenCfg.muted ?? defaults.defaultMuted ?? true); - selectedAudioReactiveEffects = !!(screenCfg.audioReactiveEffects ?? defaults.defaultAudioReactiveEffects ?? true); - selectedDisableMouse = !!(screenCfg.disableMouse ?? defaults.defaultDisableMouse ?? false); - selectedDisableParallax = !!(screenCfg.disableParallax ?? defaults.defaultDisableParallax ?? false); } function applyPendingSelection() { @@ -237,12 +812,22 @@ Item { return; } - const options = { "scaling": selectedScaling }; + const options = { "scaling": selectedScaling, "clamp": selectedClamp }; options.volume = selectedVolume; options.muted = selectedMuted; options.audioReactiveEffects = selectedAudioReactiveEffects; + options.noAutomute = !!(cfg.defaultNoAutomute ?? defaults.defaultNoAutomute ?? false); options.disableMouse = selectedDisableMouse; options.disableParallax = selectedDisableParallax; + const customProperties = {}; + for (const definition of wallpaperPropertyDefinitions) { + const propertyKey = String(definition?.key || ""); + if (propertyKey.length === 0) { + continue; + } + customProperties[propertyKey] = serializePropertyValue(propertyValueFor(definition), definition.type); + } + options.customProperties = customProperties; selectedPath = path; if (applyAllDisplays) { @@ -271,6 +856,10 @@ Item { 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 @@ -293,7 +882,42 @@ Item { } visibleWallpapers = items; - Logger.d("LWEController", "Visible wallpapers refreshed", "count=", visibleWallpapers.length, "type=", selectedType, "sort=", sortMode, "ascending=", sortAscending, "query=", query); + 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)); + + if (nextPage !== currentPage) { + currentPage = nextPage; + return; + } + + const startIndex = nextPage * safePageSize; + pagedWallpapers = visibleWallpapers.slice(startIndex, startIndex + safePageSize); + } + + function resetPagination() { + if (currentPage !== 0) { + currentPage = 0; + return; + } + + refreshPagedWallpapers(); + } + + function goToPreviousPage() { + if (currentPage > 0) { + currentPage -= 1; + } + } + + function goToNextPage() { + if (currentPage < pageCount - 1) { + currentPage += 1; + } } function reconcilePendingSelection() { @@ -362,11 +986,34 @@ Item { refreshVisibleWallpapers(); reconcilePendingSelection(); } - onSearchTextChanged: refreshVisibleWallpapers() - onSelectedTypeChanged: refreshVisibleWallpapers() - onSortModeChanged: refreshVisibleWallpapers() - onSortAscendingChanged: refreshVisibleWallpapers() + onVisibleWallpapersChanged: refreshPagedWallpapers() + onCurrentPageChanged: refreshPagedWallpapers() + onPageSizeChanged: refreshPagedWallpapers() + onSearchTextChanged: { + refreshVisibleWallpapers(); + resetPagination(); + } + onSelectedTypeChanged: { + refreshVisibleWallpapers(); + resetPagination(); + } + onSelectedResolutionChanged: { + refreshVisibleWallpapers(); + resetPagination(); + } + onSortModeChanged: { + refreshVisibleWallpapers(); + resetPagination(); + } + onSortAscendingChanged: { + refreshVisibleWallpapers(); + resetPagination(); + } onSelectedScreenNameChanged: syncSelectionOptionsFromScreen() + onPendingPathChanged: { + persistPanelMemory(); + loadWallpaperProperties(pendingPath); + } onWallpapersFolderChanged: { if (!root.pluginApi) { return; @@ -380,6 +1027,7 @@ Item { loadPanelMemory(); syncSelectionOptionsFromScreen(); scanWallpapers(); + loadWallpaperProperties(pendingPath); } Component.onDestruction: { @@ -390,6 +1038,9 @@ Item { if (filterDropdownOpen) { openFilterDropdown(); } + if (resolutionDropdownOpen) { + openResolutionDropdown(); + } if (sortDropdownOpen) { openSortDropdown(); } @@ -415,232 +1066,324 @@ Item { anchors.margins: Style.marginL spacing: Style.marginM - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: (root.applyAllDisplays || root.singleScreenMode) - ? (56 * Style.uiScaleRatio + 56 * Style.uiScaleRatio + Style.marginS * 4) - : (56 * Style.uiScaleRatio + 52 * Style.uiScaleRatio + 56 * Style.uiScaleRatio + Style.marginS * 5) - Layout.minimumHeight: Layout.preferredHeight - radius: Style.radiusL - color: Color.mSurface - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.22) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: headerColumn.implicitHeight + Style.marginS * 2 + Layout.minimumHeight: Layout.preferredHeight + radius: Style.radiusL + color: Qt.alpha(Color.mSurfaceVariant, 0.35) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.35) + + ColumnLayout { + id: headerColumn + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginS - spacing: Style.marginS + RowLayout { + Layout.fillWidth: true - RowLayout { - Layout.fillWidth: true + NIcon { + icon: "wallpaper-selector" + pointSize: Style.fontSizeL + color: Color.mOnSurface + } - 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 { - text: pluginApi?.tr("panel.title") - font.pointSize: Style.fontSizeL - font.weight: Font.Bold - color: Color.mOnSurface + 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 - icon: "refresh" - tooltipText: pluginApi?.tr("panel.reload") - onClicked: { - root.scanWallpapers(); - if (mainInstance?.hasAnyConfiguredWallpaper()) { - mainInstance?.reload(); - } else { - mainInstance.lastError = ""; - } + 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.pendingCompatibilityScan = true; } } + } - NIconButton { - enabled: mainInstance?.engineAvailable ?? false - icon: "player-stop" - tooltipText: pluginApi?.tr("panel.stop") - onClicked: mainInstance?.stopAll() + NIconButton { + enabled: mainInstance?.engineAvailable ?? false + icon: "refresh" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("panel.reload") + onClicked: { + root.scanWallpapers(); + if (mainInstance?.hasAnyConfiguredWallpaper()) { + mainInstance?.reload(true); + } else { + mainInstance.lastError = ""; + } } + } - NIconButton { - enabled: (mainInstance?.engineAvailable ?? false) && !root.singleScreenMode - icon: "device-desktop" - tooltipText: root.applyAllDisplays - ? pluginApi?.tr("panel.switchToPerDisplay") - : pluginApi?.tr("panel.switchToAllDisplays") - onClicked: root._applyAllDisplays = !root._applyAllDisplays + 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: { + if (mainInstance?.engineRunning) { + mainInstance?.stopAll(true); + } else { + mainInstance?.reload(true); + } } + } - NIconButton { - icon: "settings" - tooltipText: pluginApi?.tr("menu.settings") - onClicked: { - const screen = pluginApi?.panelOpenScreen; - BarService.openPluginSettings(screen, pluginApi?.manifest); - if (pluginApi) { - pluginApi.togglePanel(screen); - } + NIconButton { + icon: "settings" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("menu.settings") + onClicked: { + const screen = pluginApi?.panelOpenScreen; + BarService.openPluginSettings(screen, pluginApi?.manifest); + if (pluginApi) { + pluginApi.togglePanel(screen); } } + } - NIconButton { - icon: "x" - tooltipText: pluginApi?.tr("panel.closePanel") - onClicked: { - const screen = pluginApi?.panelOpenScreen; - if (pluginApi) { - pluginApi.togglePanel(screen); - } + NIconButton { + icon: "x" + colorFg: Color.mOnSurface + tooltipText: pluginApi?.tr("panel.closePanel") + onClicked: { + const screen = pluginApi?.panelOpenScreen; + if (pluginApi) { + pluginApi.togglePanel(screen); } } } + } - RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: 52 * Style.uiScaleRatio - visible: !root.applyAllDisplays && !root.singleScreenMode + NBox { + visible: root.pendingCompatibilityScan + Layout.fillWidth: true + Layout.preferredHeight: compatibilityConfirmRow.implicitHeight + Style.marginM * 2 - Repeater { - model: root.screenModel + RowLayout { + id: compatibilityConfirmRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM - NButton { - required property var modelData - Layout.fillWidth: true - enabled: mainInstance?.engineAvailable ?? false - icon: root.selectedScreenName === modelData.key ? "check" : "device-desktop" - text: modelData.name - onClicked: root.selectedScreenName = modelData.key - } + 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.startCompatibilityScan() + } + + NButton { + text: pluginApi?.tr("panel.cancel") + enabled: !root.scanningCompatibility + onClicked: root.pendingCompatibilityScan = false } } + } - RowLayout { + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 48 * Style.uiScaleRatio + + NTextInput { + id: searchInput Layout.fillWidth: true - Layout.preferredHeight: 48 * Style.uiScaleRatio + placeholderText: pluginApi?.tr("panel.searchPlaceholder") + text: root.searchText + onTextChanged: root.searchText = text + } - NTextInput { - id: searchInput - Layout.fillWidth: true - placeholderText: pluginApi?.tr("panel.searchPlaceholder") - text: root.searchText - onTextChanged: root.searchText = text + NIconButton { + Layout.alignment: Qt.AlignVCenter + visible: root.searchText.length > 0 + icon: "x" + tooltipText: pluginApi?.tr("panel.searchClear") + onClicked: root.searchText = "" + } + + Rectangle { + id: resolutionButton + Layout.preferredWidth: 172 * Style.uiScaleRatio + Layout.maximumWidth: 184 * Style.uiScaleRatio + Layout.preferredHeight: 42 * Style.uiScaleRatio + radius: Style.radiusL + color: Qt.alpha(Color.mSurfaceVariant, 0.42) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.45) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginXXS + + NIcon { + icon: "badge-hd" + pointSize: Style.fontSizeM + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + text: root.resolutionFilterLabel(root.selectedResolution) + color: Color.mOnSurface + elide: Text.ElideRight + } + + NIcon { + icon: "chevron-down" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } } - NIconButton { - Layout.alignment: Qt.AlignVCenter - visible: root.searchText.length > 0 - icon: "x" - tooltipText: pluginApi?.tr("panel.searchClear") - onClicked: root.searchText = "" + MouseArea { + anchors.fill: parent + onClicked: { + if (resolutionDropdownOpen) { + root.closeDropdowns(); + } else { + root.openResolutionDropdown(); + } + } } + } - Rectangle { - id: filterButton - Layout.preferredWidth: 172 * Style.uiScaleRatio - Layout.maximumWidth: 184 * Style.uiScaleRatio - Layout.preferredHeight: 42 * Style.uiScaleRatio - radius: Style.radiusL - color: Qt.alpha(Color.mSurfaceVariant, 0.42) - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.45) + Rectangle { + id: filterButton + Layout.preferredWidth: 172 * Style.uiScaleRatio + Layout.maximumWidth: 184 * Style.uiScaleRatio + Layout.preferredHeight: 42 * Style.uiScaleRatio + radius: Style.radiusL + color: Qt.alpha(Color.mSurfaceVariant, 0.42) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.45) - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginS - anchors.rightMargin: Style.marginS - spacing: Style.marginXXS - - NIcon { - icon: "adjustments-horizontal" - pointSize: Style.fontSizeM - color: Color.mOnSurface - } + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginXXS - NText { - Layout.fillWidth: true - text: pluginApi?.tr("panel.filterButtonSummary", { type: root.typeLabel(root.selectedType) }) - color: Color.mOnSurface - elide: Text.ElideRight - } + NIcon { + icon: "adjustments-horizontal" + pointSize: Style.fontSizeM + color: Color.mOnSurface + } - NIcon { - icon: "chevron-down" - pointSize: Style.fontSizeM - color: Color.mOnSurfaceVariant - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("panel.filterButtonSummary", { type: root.typeLabel(root.selectedType) }) + color: Color.mOnSurface + elide: Text.ElideRight } - MouseArea { - anchors.fill: parent - onClicked: { - if (filterDropdownOpen) { - root.closeDropdowns(); - } else { - root.openFilterDropdown(); - } + NIcon { + icon: "chevron-down" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (filterDropdownOpen) { + root.closeDropdowns(); + } else { + root.openFilterDropdown(); } } } + } - Rectangle { - id: sortButton - Layout.preferredWidth: 172 * Style.uiScaleRatio - Layout.maximumWidth: 184 * Style.uiScaleRatio - Layout.preferredHeight: 42 * Style.uiScaleRatio - radius: Style.radiusL - color: Qt.alpha(Color.mSurfaceVariant, 0.42) - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.45) + Rectangle { + id: sortButton + Layout.preferredWidth: 172 * Style.uiScaleRatio + Layout.maximumWidth: 184 * Style.uiScaleRatio + Layout.preferredHeight: 42 * Style.uiScaleRatio + radius: Style.radiusL + color: Qt.alpha(Color.mSurfaceVariant, 0.42) + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.45) - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginS - anchors.rightMargin: Style.marginS - spacing: Style.marginXXS - - NIcon { - icon: "arrows-sort" - pointSize: Style.fontSizeM - color: Color.mOnSurface - } + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginXXS - NText { - Layout.fillWidth: true - text: pluginApi?.tr("panel.sortButtonSummary", { - direction: root.sortAscending ? "\u2191" : "\u2193", - sort: root.sortLabel(root.sortMode) - }) - color: Color.mOnSurface - elide: Text.ElideRight - } + NIcon { + icon: "arrows-sort" + pointSize: Style.fontSizeM + color: Color.mOnSurface + } - NIcon { - icon: "chevron-down" - pointSize: Style.fontSizeM - color: Color.mOnSurfaceVariant - } + NText { + Layout.fillWidth: true + text: pluginApi?.tr("panel.sortButtonSummary", { + direction: root.sortAscending ? "\u2191" : "\u2193", + sort: root.sortLabel(root.sortMode) + }) + color: Color.mOnSurface + elide: Text.ElideRight } - MouseArea { - anchors.fill: parent - onClicked: { - if (sortDropdownOpen) { - root.closeDropdowns(); - } else { - root.openSortDropdown(); - } + NIcon { + icon: "chevron-down" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (sortDropdownOpen) { + root.closeDropdowns(); + } else { + root.openSortDropdown(); } } } } + } } } @@ -743,9 +1486,9 @@ Item { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusL - color: Color.mSurface + color: Qt.alpha(Color.mSurfaceVariant, 0.35) border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, 0.2) + border.color: Qt.alpha(Color.mOutline, 0.35) ColumnLayout { anchors.fill: parent @@ -758,21 +1501,26 @@ Item { Layout.topMargin: Style.marginXS spacing: Style.marginM - GridView { - id: gridView + ColumnLayout { 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((width + cardGap) / (minCardWidth + cardGap))) - cellWidth: (width - ((columnCount - 1) * cardGap)) / columnCount - cellHeight: 208 * Style.uiScaleRatio - boundsBehavior: Flickable.StopAtBounds - clip: 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.visibleWallpapers + model: root.pagedWallpapers - delegate: Rectangle { + delegate: Rectangle { id: tileCard required property var modelData width: gridView.cellWidth @@ -847,51 +1595,122 @@ Item { } } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginXS + 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 + } + } + + RowLayout { + 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 { - Layout.fillWidth: true - text: modelData.name - color: Color.mOnSurface - elide: Text.ElideRight + id: typeBadgeText + anchors.centerIn: parent + text: 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 { - visible: modelData.dynamic - text: pluginApi?.tr("panel.dynamicBadge") - color: Color.mPrimary - font.pointSize: Style.fontSizeS + 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 } + } - NIcon { - visible: root.selectedPath === modelData.path - icon: "check" - pointSize: Style.fontSizeL - color: Color.mPrimary + Item { Layout.fillWidth: true } + + Rectangle { + visible: 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 { + id: resolutionBadgeIconItem + icon: root.resolutionBadgeIcon(modelData.resolution) + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + text: root.resolutionBadgeLabel(modelData.resolution) + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + } } } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginXS + Rectangle { + visible: root.wallpaperPropertyLoadFailedByPath[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 - NText { - Layout.fillWidth: true - text: modelData.id - color: Color.mOnSurfaceVariant - elide: Text.ElideMiddle - font.pointSize: Style.fontSizeS - } + NIcon { + icon: "alert-triangle" + pointSize: Style.fontSizeM + color: Color.mError + } - NText { - text: root.typeLabel(modelData.type) - color: Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeS + NText { + text: pluginApi?.tr("panel.propertiesFailedBadge") + color: Color.mError + font.pointSize: Style.fontSizeXS + font.weight: Font.Medium + } } } + + } } MouseArea { @@ -903,28 +1722,82 @@ Item { } } - Rectangle { - visible: root.visibleWallpapers.length === 0 && !root.scanningWallpapers - anchors.centerIn: parent - color: "transparent" - width: 300 * Style.uiScaleRatio - height: 140 * Style.uiScaleRatio - - ColumnLayout { + Rectangle { + visible: root.visibleWallpapers.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.wallpaperItems.length === 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.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 - NIcon { - Layout.alignment: Qt.AlignHCenter - icon: "photo" - pointSize: Style.fontSizeXL - color: Color.mOnSurfaceVariant + NButton { + text: pluginApi?.tr("panel.prevPage") + icon: "chevron-left" + enabled: root.currentPage > 0 + onClicked: root.goToPreviousPage() + } + + NText { + text: pluginApi?.tr("panel.pageSummary", { + current: root.currentPageDisplay, + total: root.pageCount + }) + color: Color.mOnSurface + font.weight: Font.Medium } NText { - text: pluginApi?.tr("panel.empty") + text: pluginApi?.tr("panel.pageRange", { + start: root.currentPageStartIndex, + end: root.currentPageEndIndex, + total: root.visibleWallpapers.length + }) color: Color.mOnSurfaceVariant } + + Item { Layout.fillWidth: true } + + NButton { + text: pluginApi?.tr("panel.nextPage") + icon: "chevron-right" + enabled: root.currentPage < root.pageCount - 1 + onClicked: root.goToNextPage() + } } } } @@ -993,29 +1866,108 @@ Item { elide: Text.ElideRight } - NText { + RowLayout { Layout.fillWidth: true - text: root.selectedWallpaperData ? root.selectedWallpaperData.id : "" - color: Color.mOnSurfaceVariant - elide: Text.ElideMiddle - font.pointSize: Style.fontSizeS - } + spacing: Style.marginXS - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginM - Layout.bottomMargin: Style.marginM - } + Rectangle { + visible: root.selectedWallpaperData && 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.selectedWallpaperData.resolution) : "" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + id: sidebarResolutionBadgeText + text: root.selectedWallpaperData ? 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.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.wallpaperPropertyLoadFailedByPath[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 + } + } + } - NText { - text: pluginApi?.tr("panel.sectionInfo") - color: Color.mOnSurface - font.weight: Font.Bold - font.pointSize: Style.fontSizeM } RowLayout { Layout.fillWidth: true + Layout.topMargin: Style.marginM NText { text: pluginApi?.tr("panel.infoType") @@ -1040,10 +1992,37 @@ Item { Item { Layout.fillWidth: true } - NText { - text: root.selectedWallpaperData ? root.selectedWallpaperData.id : "" - color: Color.mOnSurface - elide: Text.ElideMiddle + Rectangle { + color: "transparent" + implicitWidth: idValueText.implicitWidth + implicitHeight: idValueText.implicitHeight + + NText { + id: idValueText + text: root.selectedWallpaperData ? root.selectedWallpaperData.id : "" + color: idLinkArea.containsMouse ? Color.mPrimary : Color.mOnSurface + elide: Text.ElideMiddle + } + + MouseArea { + id: idLinkArea + anchors.fill: parent + hoverEnabled: true + enabled: root.workshopUrlForWallpaper(root.selectedWallpaperData).length > 0 + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + const workshopUrl = root.workshopUrlForWallpaper(root.selectedWallpaperData); + if (workshopUrl.length === 0) { + return; + } + + const screen = pluginApi?.panelOpenScreen; + if (pluginApi) { + pluginApi.togglePanel(screen); + } + Qt.openUrlExternally(workshopUrl); + } + } } } @@ -1083,104 +2062,377 @@ Item { } } - NDivider { + ColumnLayout { Layout.fillWidth: true - Layout.topMargin: Style.marginM - Layout.bottomMargin: Style.marginM - } + spacing: Style.marginS - NText { - text: pluginApi?.tr("panel.sectionAudio") - color: Color.mOnSurface - font.weight: Font.Bold - font.pointSize: Style.fontSizeM - } + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NButton { + Layout.fillWidth: true + text: pluginApi?.tr("panel.confirmApply") + icon: "check" + enabled: (mainInstance?.engineAvailable ?? false) && root.pendingPath.length > 0 + onClicked: root.applyPendingSelection() + } - NComboBox { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperScaling") - 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.selectedScaling - onSelected: key => root.selectedScaling = key - } + NIconButton { + Layout.preferredWidth: 42 * Style.uiScaleRatio + Layout.preferredHeight: 42 * Style.uiScaleRatio + visible: !root.singleScreenMode + enabled: mainInstance?.engineAvailable ?? false + icon: "device-desktop" + tooltipText: root.applyAllDisplays + ? pluginApi?.tr("panel.targetAllDisplays") + : pluginApi?.tr("panel.targetSingleDisplay", { screen: root.selectedScreenName }) + onClicked: root.applyTargetExpanded = !root.applyTargetExpanded + } + } - NSpinBox { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperVolume") - from: 0 - to: 100 - suffix: " %" - value: root.selectedVolume - enabled: !root.selectedMuted - onValueChanged: root.selectedVolume = value - } + NBox { + Layout.fillWidth: true + visible: !root.singleScreenMode && root.applyTargetExpanded + Layout.preferredHeight: targetScreenColumn.implicitHeight + Style.marginL * 2 - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperMuted") - checked: root.selectedMuted - onToggled: checked => root.selectedMuted = checked - } + ButtonGroup { + id: targetScreenGroup + } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginM - Layout.bottomMargin: Style.marginM - } + ColumnLayout { + id: targetScreenColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginS + + NRadioButton { + ButtonGroup.group: targetScreenGroup + Layout.fillWidth: true + enabled: mainInstance?.engineAvailable ?? false + text: pluginApi?.tr("panel.applyAllDisplays") + checked: root.applyAllDisplays + onClicked: { + root._applyAllDisplays = true; + root.applyTargetExpanded = false; + } + } - NText { - text: pluginApi?.tr("panel.sectionFeatures") - color: Color.mOnSurface - font.weight: Font.Bold - font.pointSize: Style.fontSizeM - } + Repeater { + model: root.screenModel + + NRadioButton { + ButtonGroup.group: targetScreenGroup + required property var modelData + Layout.fillWidth: true + enabled: mainInstance?.engineAvailable ?? false + text: pluginApi?.tr("panel.applySingleDisplay", { screen: modelData.name }) + checked: !root.applyAllDisplays && root.selectedScreenName === modelData.key + onClicked: { + root._applyAllDisplays = false; + root.selectedScreenName = modelData.key; + root.applyTargetExpanded = false; + } + } + } + } + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperAudioReactive") - checked: root.selectedAudioReactiveEffects - onToggled: checked => root.selectedAudioReactiveEffects = checked - } + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperDisableMouse") - checked: root.selectedDisableMouse - onToggled: checked => root.selectedDisableMouse = checked - } + NText { + text: pluginApi?.tr("panel.sectionAudio") + color: Color.mOnSurface + font.weight: Font.Bold + font.pointSize: Style.fontSizeM + } - NToggle { - Layout.fillWidth: true - label: pluginApi?.tr("panel.wallpaperDisableParallax") - checked: root.selectedDisableParallax - onToggled: checked => root.selectedDisableParallax = checked - } + NComboBox { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperScaling") + 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.selectedScaling + onSelected: key => root.selectedScaling = key + } - NText { - Layout.fillWidth: true - text: pluginApi?.tr("panel.pendingHint") - color: Color.mOnSurfaceVariant - wrapMode: Text.Wrap - } + NComboBox { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperClamp") + model: [ + { "key": "clamp", "name": pluginApi?.tr("panel.clampClamp") }, + { "key": "border", "name": pluginApi?.tr("panel.clampBorder") }, + { "key": "repeat", "name": pluginApi?.tr("panel.clampRepeat") } + ] + currentKey: root.selectedClamp + onSelected: key => root.selectedClamp = key + } - NButton { - Layout.fillWidth: true - text: pluginApi?.tr("panel.confirmApply") - icon: "check" - enabled: (mainInstance?.engineAvailable ?? false) && root.pendingPath.length > 0 - onClicked: root.applyPendingSelection() - } + NSpinBox { + id: wallpaperVolumeSpinBox + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperVolume") + from: 0 + to: 100 + stepSize: 1 + suffix: pluginApi?.tr("settings.units.percent") + value: root.selectedVolume + enabled: !root.selectedMuted + onValueChanged: if (value !== root.selectedVolume) root.selectedVolume = value + } - NButton { - Layout.fillWidth: true - text: pluginApi?.tr("panel.resetWallpaperSettings") - icon: "refresh" - onClicked: root.resetPendingToGlobalDefaults() + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperMuted") + checked: root.selectedMuted + onToggled: checked => root.selectedMuted = checked + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + NText { + text: pluginApi?.tr("panel.sectionFeatures") + color: Color.mOnSurface + font.weight: Font.Bold + font.pointSize: Style.fontSizeM + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperAudioReactive") + checked: root.selectedAudioReactiveEffects + onToggled: checked => root.selectedAudioReactiveEffects = checked + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperDisableMouse") + checked: root.selectedDisableMouse + onToggled: checked => root.selectedDisableMouse = checked + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("panel.wallpaperDisableParallax") + checked: root.selectedDisableParallax + onToggled: checked => root.selectedDisableParallax = checked + } + + ColumnLayout { + Layout.fillWidth: true + visible: root.extraPropertiesEditorEnabled + spacing: Style.marginS + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + NText { + text: pluginApi?.tr("panel.sectionProperties") + color: Color.mOnSurface + font.weight: Font.Bold + font.pointSize: Style.fontSizeM + } + + NText { + visible: root.loadingWallpaperProperties + Layout.fillWidth: true + text: pluginApi?.tr("panel.loadingProperties") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } + + NText { + visible: !root.loadingWallpaperProperties && root.wallpaperPropertyError.length > 0 + Layout.fillWidth: true + text: root.wallpaperPropertyError + color: Color.mError + wrapMode: Text.Wrap + } + + NText { + visible: !root.loadingWallpaperProperties && root.wallpaperPropertyError.length === 0 && root.wallpaperPropertyDefinitions.length === 0 + Layout.fillWidth: true + text: pluginApi?.tr("panel.noEditableProperties") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } + + NText { + visible: !root.loadingWallpaperProperties && root.wallpaperPropertyDefinitions.length > 0 + Layout.fillWidth: true + text: pluginApi?.tr("panel.propertiesNotice") + color: Color.mOnSurfaceVariant + wrapMode: Text.Wrap + } + + Repeater { + model: root.wallpaperPropertyDefinitions + + delegate: ColumnLayout { + id: propertyEditor + required property var modelData + Layout.fillWidth: true + spacing: Style.marginXS + + property bool boolValue: !!root.propertyValueFor(modelData) + property real sliderValue: root.numberOr(root.propertyValueFor(modelData), 0) + property string comboValue: String(root.propertyValueFor(modelData)) + property string textValue: String(root.propertyValueFor(modelData)) + property color colorValue: Qt.rgba(1, 1, 1, 1) + + Component.onCompleted: { + if (modelData.type === "color") { + propertyEditor.colorValue = root.ensureColorValue(root.propertyValueFor(modelData)); + } + } + + NToggle { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "boolean" + label: modelData.label + checked: propertyEditor.boolValue + onToggled: checked => { + if (checked === propertyEditor.boolValue) { + return; + } + propertyEditor.boolValue = checked; + root.setPropertyValue(modelData.key, checked); + } + } + + NValueSlider { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "slider" + label: modelData.label + from: root.numberOr(modelData.min, 0) + to: root.numberOr(modelData.max, 100) + stepSize: Math.max(root.numberOr(modelData.step, 1), 0.001) + value: propertyEditor.sliderValue + text: root.formatSliderValue(propertyEditor.sliderValue, modelData.step) + onMoved: value => { + if (value === propertyEditor.sliderValue) { + return; + } + propertyEditor.sliderValue = value; + root.setPropertyValue(modelData.key, value); + } + } + + NComboBox { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "combo" + label: modelData.label + model: root.comboChoicesFor(modelData) + currentKey: propertyEditor.comboValue + onSelected: key => { + const normalizedKey = String(key); + if (normalizedKey === propertyEditor.comboValue) { + return; + } + propertyEditor.comboValue = normalizedKey; + root.setPropertyValue(modelData.key, normalizedKey); + } + } + + NTextInput { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "textinput" + label: modelData.label + text: propertyEditor.textValue + onEditingFinished: { + const nextText = String(text); + if (nextText === propertyEditor.textValue) { + return; + } + propertyEditor.textValue = nextText; + root.setPropertyValue(modelData.key, nextText); + } + onAccepted: { + const nextText = String(text); + if (nextText === propertyEditor.textValue) { + return; + } + propertyEditor.textValue = nextText; + root.setPropertyValue(modelData.key, nextText); + } + } + + NText { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "text" + text: modelData.label + color: Color.mPrimary + font.pointSize: Style.fontSizeM + font.weight: Font.Bold + wrapMode: Text.Wrap + topPadding: Style.marginXS + bottomPadding: Style.marginXS + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + visible: modelData.type === "color" + spacing: Style.marginXS + + NText { + Layout.fillWidth: true + text: modelData.label + color: Color.mOnSurface + font.pointSize: Style.fontSizeM + wrapMode: Text.Wrap + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + radius: Style.radiusM + color: propertyEditor.colorValue + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, 0.35) + } + + NColorPicker { + screen: pluginApi?.panelOpenScreen + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + selectedColor: propertyEditor.colorValue + onColorSelected: color => { + propertyEditor.colorValue = color; + root.setPropertyValue(modelData.key, color); + } + } + + NText { + Layout.fillWidth: true + text: root.serializePropertyValue(propertyEditor.colorValue, "color") + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeS + } + } + } + } + } } } } @@ -1203,23 +2455,75 @@ Item { wrapMode: Text.WrapAnywhere } - NText { - visible: root.scanningWallpapers - text: pluginApi?.tr("panel.scanning") - color: Color.mOnSurfaceVariant + NText { + visible: root.scanningWallpapers + text: pluginApi?.tr("panel.scanning") + color: Color.mOnSurfaceVariant + } } - } } MouseArea { anchors.fill: parent - visible: root.filterDropdownOpen || root.sortDropdownOpen + visible: root.filterDropdownOpen || root.resolutionDropdownOpen || root.sortDropdownOpen z: 900 acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: root.closeDropdowns() } + 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.applyResolutionFilterAction(modelData.action) + } + } + } + } + Rectangle { visible: root.filterDropdownOpen x: root.filterDropdownX @@ -1248,7 +2552,7 @@ Item { delegate: Rectangle { required property var modelData - width: filterList.width + width: filterList.availableWidth height: 34 * Style.uiScaleRatio radius: Style.radiusM color: modelData.selected ? Qt.alpha(Color.mPrimary, 0.22) : "transparent" @@ -1307,7 +2611,7 @@ Item { delegate: Rectangle { required property var modelData - width: sortList.width + width: sortList.availableWidth height: 34 * Style.uiScaleRatio radius: Style.radiusM color: modelData.selected ? Qt.alpha(Color.mPrimary, 0.22) : "transparent" @@ -1332,6 +2636,103 @@ Item { } } + Process { + id: wallpaperPropertyProcess + + stdout: StdioCollector { + id: wallpaperPropertyStdout + } + + stderr: StdioCollector { + id: wallpaperPropertyStderr + } + + onExited: function(exitCode) { + const requestPath = root.wallpaperPropertyRequestPath; + root.loadingWallpaperProperties = false; + + const outputText = [String(wallpaperPropertyStdout.text || ""), String(wallpaperPropertyStderr.text || "")] + .filter(part => part.trim().length > 0) + .join("\n"); + + if (requestPath.length === 0 || requestPath !== String(root.pendingPath || "")) { + Logger.d("LWEController", "Ignoring stale wallpaper property result", "requestPath=", requestPath, "pendingPath=", root.pendingPath, "exitCode=", exitCode); + return; + } + + if (exitCode !== 0) { + root.wallpaperPropertyDefinitions = []; + 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); + return; + } + + const definitions = root.parseWallpaperPropertiesOutput(outputText); + root.setWallpaperPropertyLoadFailed(requestPath, false); + 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))); + } + } + + const savedProperties = mainInstance?.getWallpaperProperties(requestPath) || ({}); + const nextValues = {}; + for (const definition of definitions) { + const propertyKey = String(definition.key || ""); + if (savedProperties[propertyKey] !== undefined) { + nextValues[propertyKey] = root.parsePropertyValue(savedProperties[propertyKey], definition.type); + } else { + nextValues[propertyKey] = definition.defaultValue; + } + } + root.wallpaperPropertyValues = nextValues; + root.wallpaperPropertyError = ""; + Logger.i("LWEController", "Wallpaper properties loaded", "path=", requestPath, "count=", definitions.length); + } + } + + Process { + id: compatibilityScanProcess + + stdout: StdioCollector { + id: compatibilityScanStdout + } + + stderr: StdioCollector { + id: compatibilityScanStderr + } + + onExited: function(exitCode) { + root.scanningCompatibility = false; + + const stdoutText = String(compatibilityScanStdout.text || ""); + 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); + } + return; + } + + const result = root.applyCompatibilityScanOutput(stdoutText); + Logger.i("LWEController", "Compatibility scan completed", "totalCount=", result.totalCount, "failedCount=", result.failedCount); + ToastService.showNotice( + pluginApi?.tr("panel.title"), + pluginApi?.tr("panel.compatibilityQuickCheckFinished", { + total: result.totalCount, + failed: result.failedCount + }), + result.failedCount > 0 ? "alert-triangle" : "check" + ); + } + } + Process { id: scanProcess diff --git a/linux-wallpaperengine-controller/README.md b/linux-wallpaperengine-controller/README.md index c4acabc3..8b62cf0a 100644 --- a/linux-wallpaperengine-controller/README.md +++ b/linux-wallpaperengine-controller/README.md @@ -1,26 +1,20 @@ # Linux WallpaperEngine Controller -A lightweight Noctalia plugin to browse and apply `linux-wallpaperengine` wallpapers. +A Noctalia plugin that provides a Wallpaper-Engine wallpaper selector powered by your locally installed `linux-wallpaperengine`, with multi-display targeting, runtime controls, extra property editing, and compatibility checks. -Use it directly from the bar and panel to switch wallpapers quickly, with per-screen control and simple playback options. +## Features -## Highlights - -- Apply wallpapers per display or to all displays -- Quick search, filter, and sort in panel -- Per-wallpaper options (scaling, volume, mute, audio reactive) -- One-click engine reload/stop from bar menu +- 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 +- 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 +- Runtime controls for scaling, clamp mode, volume, mute, audio reactive effects, mouse input, and parallax +- 5 translations: en, ja, ru, zh-CN, zh-TW ## Requirements - [linux-wallpaperengine](https://github.com/Almamu/linux-wallpaperengine) installed and available in `PATH` -- Wallpaper Engine projects available in your Steam Workshop folder - -## Quick Start - -1. Open plugin settings and set `Wallpapers source folder` -2. Open the plugin panel from the bar icon -3. Select a wallpaper and click `Apply` +- Wallpaper Engine workshop projects available in your Steam Workshop folder ## IPC Commands @@ -44,13 +38,17 @@ qs ipc call plugin:linux-wallpaperengine-controller stop all qs ipc call plugin:linux-wallpaperengine-controller reload ``` -## Basic Troubleshooting +## Troubleshooting -- Check binary in PATH: `command -v linux-wallpaperengine` -- If panel shows folder error: verify `Wallpapers source folder` exists and contains wallpaper project folders -- If engine fails to start: recheck dependencies and GPU/OpenGL environment -- For runtime logs: start shell with debug: `NOCTALIA_DEBUG=1 qs -c noctalia-shell` +- Check that `linux-wallpaperengine` is available: `command -v linux-wallpaperengine` +- If the panel shows a source-folder error, verify that `Wallpapers source folder` exists and contains Wallpaper Engine project directories +- If no wallpapers appear after applying filters, clear the search text and resolution/type filters +- If a wallpaper is marked as `may fail`, run the compatibility quick check again and verify that `linux-wallpaperengine --list-properties ` succeeds +- If the extra properties section is empty, that wallpaper may not expose supported editable properties +- If the engine fails to start, recheck your GPU / OpenGL environment. +- For runtime logs, start the shell with debug enabled: `NOCTALIA_DEBUG=1 qs -c noctalia-shell` ## Notes -- This plugin controls the `linux-wallpaperengine` process and does not ship wallpapers itself. +- This plugin does not bundle the wallpaper engine or any wallpapers. It works by calling your locally installed `linux-wallpaperengine` and using Wallpaper Engine workshop wallpapers you have already downloaded. +- If no wallpaper matches the current search or filters, the panel will show a filtered empty state instead of the generic source-folder message. diff --git a/linux-wallpaperengine-controller/Settings.qml b/linux-wallpaperengine-controller/Settings.qml index ed742770..ffcee10b 100644 --- a/linux-wallpaperengine-controller/Settings.qml +++ b/linux-wallpaperengine-controller/Settings.qml @@ -15,11 +15,15 @@ ColumnLayout { 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" + property string editDefaultClamp: cfg.defaultClamp ?? defaults.defaultClamp ?? "clamp" property int editDefaultFps: cfg.defaultFps ?? defaults.defaultFps ?? 30 property int editDefaultVolume: cfg.defaultVolume ?? defaults.defaultVolume ?? 100 property bool editDefaultMuted: cfg.defaultMuted ?? defaults.defaultMuted ?? true property bool editDefaultAudioReactiveEffects: cfg.defaultAudioReactiveEffects ?? defaults.defaultAudioReactiveEffects ?? true + property bool editDefaultNoAutomute: cfg.defaultNoAutomute ?? defaults.defaultNoAutomute ?? false property bool editDefaultDisableMouse: cfg.defaultDisableMouse ?? defaults.defaultDisableMouse ?? false property bool editDefaultDisableParallax: cfg.defaultDisableParallax ?? defaults.defaultDisableParallax ?? false property bool editDefaultNoFullscreenPause: cfg.defaultNoFullscreenPause ?? defaults.defaultNoFullscreenPause ?? false @@ -31,27 +35,49 @@ ColumnLayout { NText { Layout.fillWidth: true - text: pluginApi?.tr("settings.category.performanceTitle") + text: pluginApi?.tr("settings.category.interfaceTitle") color: Color.mOnSurface font.weight: Font.Bold } - RowLayout { + 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.defaultFps.label") - color: Color.mOnSurface - } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.enableExtraPropertiesEditor.label") + description: pluginApi?.tr("settings.enableExtraPropertiesEditor.description") + checked: root.editEnableExtraPropertiesEditor + onToggled: checked => root.editEnableExtraPropertiesEditor = checked + } - NSpinBox { - from: 1 - to: 240 - value: root.editDefaultFps - suffix: " FPS" - onValueChanged: root.editDefaultFps = value - } + NDivider { + Layout.fillWidth: true + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("settings.category.performanceTitle") + color: Color.mOnSurface + font.weight: Font.Bold + } + + 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 { @@ -78,6 +104,10 @@ ColumnLayout { onToggled: checked => root.editAutoApplyOnStartup = checked } + NDivider { + Layout.fillWidth: true + } + NText { Layout.fillWidth: true text: pluginApi?.tr("settings.category.compatibilityTitle") @@ -118,6 +148,10 @@ ColumnLayout { onTextChanged: root.editAssetsDir = text } + NDivider { + Layout.fillWidth: true + } + NText { Layout.fillWidth: true text: pluginApi?.tr("settings.category.audioTitle") @@ -134,14 +168,16 @@ ColumnLayout { } NSpinBox { + id: defaultVolumeSpinBox Layout.fillWidth: true label: pluginApi?.tr("settings.defaultVolume.label") from: 0 to: 100 - suffix: " %" + stepSize: 1 + suffix: pluginApi?.tr("settings.units.percent") value: root.editDefaultVolume enabled: !root.editDefaultMuted - onValueChanged: root.editDefaultVolume = value + onValueChanged: if (value !== root.editDefaultVolume) root.editDefaultVolume = value } NToggle { @@ -151,6 +187,18 @@ ColumnLayout { onToggled: checked => root.editDefaultAudioReactiveEffects = 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 + } + + NDivider { + Layout.fillWidth: true + } + NText { Layout.fillWidth: true text: pluginApi?.tr("settings.category.displayTitle") @@ -172,6 +220,19 @@ ColumnLayout { onSelected: key => root.editDefaultScaling = key } + 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") @@ -198,11 +259,15 @@ 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; - pluginApi.pluginSettings.defaultFps = root.editDefaultFps; - pluginApi.pluginSettings.defaultVolume = root.editDefaultVolume; + pluginApi.pluginSettings.defaultClamp = root.editDefaultClamp; + pluginApi.pluginSettings.defaultFps = defaultFpsSpinBox.value; + pluginApi.pluginSettings.defaultVolume = defaultVolumeSpinBox.value; pluginApi.pluginSettings.defaultMuted = root.editDefaultMuted; pluginApi.pluginSettings.defaultAudioReactiveEffects = root.editDefaultAudioReactiveEffects; + pluginApi.pluginSettings.defaultNoAutomute = root.editDefaultNoAutomute; pluginApi.pluginSettings.defaultDisableMouse = root.editDefaultDisableMouse; pluginApi.pluginSettings.defaultDisableParallax = root.editDefaultDisableParallax; pluginApi.pluginSettings.defaultNoFullscreenPause = root.editDefaultNoFullscreenPause; @@ -210,7 +275,7 @@ ColumnLayout { pluginApi.pluginSettings.autoApplyOnStartup = root.editAutoApplyOnStartup; pluginApi.saveSettings(); - Logger.d("LWEController", "Settings saved", "wallpapersFolder=", root.editWallpapersFolder, "assetsDir=", root.editAssetsDir, "defaultScaling=", root.editDefaultScaling, "defaultFps=", root.editDefaultFps, "defaultVolume=", root.editDefaultVolume, "defaultMuted=", root.editDefaultMuted, "defaultAudioReactiveEffects=", root.editDefaultAudioReactiveEffects, "defaultDisableMouse=", root.editDefaultDisableMouse, "defaultDisableParallax=", root.editDefaultDisableParallax, "defaultNoFullscreenPause=", root.editDefaultNoFullscreenPause, "defaultFullscreenPauseOnlyActive=", root.editDefaultFullscreenPauseOnlyActive, "autoApplyOnStartup=", root.editAutoApplyOnStartup); + 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); if (pluginApi.mainInstance && pluginApi.mainInstance.engineAvailable) { Logger.d("LWEController", "Triggering engine reload after settings save"); diff --git a/linux-wallpaperengine-controller/i18n/en.json b/linux-wallpaperengine-controller/i18n/en.json index d23b1cc6..d6177dc0 100644 --- a/linux-wallpaperengine-controller/i18n/en.json +++ b/linux-wallpaperengine-controller/i18n/en.json @@ -9,9 +9,15 @@ }, "menu": { "reload": "Reload wallpaper engine", + "start": "Start wallpaper engine", "stop": "Stop wallpaper engine", "settings": "Plugin settings" }, + "toast": { + "reloaded": "Wallpaper engine reloaded", + "stopped": "Wallpaper engine stopped", + "reloadSkippedNoWallpaper": "No wallpaper is configured, so there was nothing to reload" + }, "main": { "status": { "unavailable": "Engine unavailable", @@ -29,44 +35,50 @@ } }, "panel": { - "title": "Linux-WallpaperEngine Settings", - "sectionInfo": "Information", - "sectionAudio": "Audio & Display", + "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", "errorDismiss": "Dismiss", - "screen": { - "label": "Target screen", - "description": "Select the monitor to apply the current wallpaper" - }, "reload": "Reload", + "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", "folderInvalid": "Wallpaper source directory is invalid or inaccessible.", "scanning": "Scanning wallpapers...", - "searchPlaceholder": "Search wallpapers...", + "searchPlaceholder": "Search wallpapers by name or ID", "searchClear": "Clear search", - "filterButton": "Filter", "filterButtonSummary": "Filter · {type}", - "sortButton": "Sort", "sortButtonSummary": "Sort · {direction} {sort}", "applyAllDisplays": "Apply to all displays", - "perDisplayMode": "Multi-display", - "switchToAllDisplays": "Apply on all displays", - "switchToPerDisplay": "Apply separately per display", "closePanel": "Close panel", - "confirmApply": "Confirm apply", - "cancelSelection": "Cancel", - "pendingHint": "Selected but not applied yet. You can adjust settings before confirming.", + "confirmApply": "Apply wallpaper", "wallpaperScaling": "Scaling", + "wallpaperClamp": "Clamp mode", "wallpaperVolume": "Volume", "wallpaperMuted": "Mute", - "wallpaperAudioReactive": "Audio reactive", + "wallpaperAudioReactive": "Audio reactive effects", "wallpaperDisableMouse": "Disable mouse interaction", "wallpaperDisableParallax": "Disable parallax effect", - "resetWallpaperSettings": "Reset to global defaults", + "sectionProperties": "Extra properties", + "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", + "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", "infoResolution": "Resolution", @@ -75,8 +87,13 @@ "scalingFit": "Fit", "scalingStretch": "Stretch", "scalingDefault": "Default", + "clampClamp": "Clamp", + "clampBorder": "Border", + "clampRepeat": "Repeat", + "propertyLabelThemeColor": "Theme Color", "targetAllDisplays": "Target: All displays", "targetSingleDisplay": "Target: {screen}", + "applySingleDisplay": "Apply to {screen}", "filterAll": "All", "filterTypeAll": "All", "filterTypeScene": "Scene", @@ -84,6 +101,8 @@ "filterTypeWeb": "Web", "filterTypeApplication": "Application", "filterResAll": "Resolution: All", + "filterRes4k": "Resolution: 4K", + "filterRes8k": "Resolution: 8K", "filterResUnknown": "Resolution: Unknown", "filterStatic": "Static", "filterDynamic": "Dynamic", @@ -100,17 +119,35 @@ "typeApplication": "Application", "resolutionUnknown": "Unknown", "dynamicBadge": "Dynamic", + "staticBadge": "Static", + "prevPage": "Previous", + "nextPage": "Next", + "pageSummary": "Page {current} / {total}", + "pageRange": "{start}-{end} / {total}", "count": "Wallpaper count: {count}", - "apply": "Apply", - "empty": "No folders found in source directory" + "emptyAll": "No wallpapers found in the source directory", + "emptyFiltered": "No wallpapers match the current filters" }, "settings": { "category": { + "interfaceTitle": "Interface", "performanceTitle": "Performance", - "compatibilityTitle": "Compatibility", + "compatibilityTitle": "Resources", "audioTitle": "Audio", "displayTitle": "Display" }, + "iconColor": { + "label": "Icon color", + "description": "Color used for the bar widget icon" + }, + "enableExtraPropertiesEditor": { + "label": "Enable extra property editor", + "description": "Show wallpaper-specific extra properties in the panel sidebar when available" + }, + "units": { + "fps": " FPS", + "percent": " %" + }, "wallpapersFolder": { "label": "Wallpaper source directory", "description": "Directory containing wallpaper project folders", @@ -123,9 +160,13 @@ "placeholder": "~/.local/share/wallpaperengine/assets" }, "defaultScaling": { - "label": "Default scaling", + "label": "Default scaling mode", "description": "Default scaling mode when no custom value is set" }, + "defaultClamp": { + "label": "Default clamp mode", + "description": "Default edge handling mode used when rendering wallpapers" + }, "defaultFps": { "label": "Default FPS", "description": "Default frame rate limit for wallpapers", @@ -149,6 +190,10 @@ "defaultAudioReactiveEffects": { "label": "Audio reactive effects" }, + "defaultNoAutomute": { + "label": "Do not mute when other apps play audio", + "description": "When enabled, wallpaper audio keeps playing even while other applications are outputting sound" + }, "defaultDisableMouse": { "label": "Disable mouse interaction" }, diff --git a/linux-wallpaperengine-controller/i18n/ja.json b/linux-wallpaperengine-controller/i18n/ja.json index e31d299e..f5b72646 100644 --- a/linux-wallpaperengine-controller/i18n/ja.json +++ b/linux-wallpaperengine-controller/i18n/ja.json @@ -9,9 +9,15 @@ }, "menu": { "reload": "壁紙エンジンを再読み込み", + "start": "壁紙エンジンを開始", "stop": "壁紙エンジンを停止", "settings": "プラグイン設定" }, + "toast": { + "reloaded": "壁紙エンジンを再読み込みしました", + "stopped": "壁紙エンジンを停止しました", + "reloadSkippedNoWallpaper": "壁紙が設定されていないため、再読み込みする内容がありません" + }, "main": { "status": { "unavailable": "エンジンは利用できません", @@ -29,44 +35,50 @@ } }, "panel": { - "title": "Linux-WallpaperEngine 設定", - "sectionInfo": "情報", - "sectionAudio": "オーディオとディスプレイ", + "title": "Wallpaper-Engine 壁紙セレクター", + "statusChecking": "確認中", + "statusUnavailable": "利用不可", + "statusRunning": "実行中", + "statusReady": "設定済み", + "statusStopped": "停止", + "sectionAudio": "ディスプレイとオーディオ", "sectionFeatures": "機能", "installHint": "インストール例: yay -S linux-wallpaperengine-git", "errorBannerTitle": "ランタイムエラーを検出", "errorShowDetails": "詳細を表示", "errorHideDetails": "詳細を隠す", "errorDismiss": "閉じる", - "screen": { - "label": "対象スクリーン", - "description": "現在の壁紙を適用するモニターを選択" - }, "reload": "再読み込み", + "start": "開始", "stop": "停止", + "confirm": "確認", + "cancel": "キャンセル", + "compatibilityQuickCheck": "互換性クイックチェック", + "compatibilityQuickCheckRunning": "壁紙の互換性を確認中...", + "compatibilityQuickCheckConfirm": "すべての壁紙の追加プロパティ互換性をスキャンしますか? 少し時間がかかる場合があります。", + "compatibilityQuickCheckFinished": "{total} 件の壁紙をスキャンし、{failed} 件が要注意です", "folderInvalid": "壁紙ソースディレクトリが無効、またはアクセスできません。", "scanning": "壁紙をスキャン中...", - "searchPlaceholder": "壁紙を検索...", + "searchPlaceholder": "名前または ID で壁紙を検索", "searchClear": "検索をクリア", - "filterButton": "フィルター", "filterButtonSummary": "フィルター · {type}", - "sortButton": "並び替え", "sortButtonSummary": "並び替え · {direction}{sort}", "applyAllDisplays": "すべてのディスプレイに適用", - "perDisplayMode": "マルチディスプレイ", - "switchToAllDisplays": "すべてのディスプレイに適用", - "switchToPerDisplay": "ディスプレイごとに適用", "closePanel": "パネルを閉じる", - "confirmApply": "適用を確認", - "cancelSelection": "キャンセル", - "pendingHint": "選択済みですが未適用です。設定を調整してから確定できます。", + "confirmApply": "壁紙を適用", "wallpaperScaling": "拡大縮小", + "wallpaperClamp": "クランプモード", "wallpaperVolume": "音量", "wallpaperMuted": "ミュート", - "wallpaperAudioReactive": "オーディオ反応", + "wallpaperAudioReactive": "オーディオ反応効果", "wallpaperDisableMouse": "マウス操作を無効化", "wallpaperDisableParallax": "パララックス効果を無効化", - "resetWallpaperSettings": "グローバル既定値にリセット", + "sectionProperties": "追加プロパティ", + "loadingProperties": "壁紙プロパティを読み込み中...", + "propertiesLoadFailed": "壁紙プロパティの読み込みに失敗しました。この壁紙のプロパティ形式はまだサポートされていない可能性があるか、この壁紙自体が正しく読み込めず実行できない可能性があります", + "propertiesFailedBadge": "要注意", + "noEditableProperties": "この壁紙には、今のところ編集可能な追加プロパティがありません", + "propertiesNotice": "ここには、このプラグインで直接編集できるプロパティのみ表示されます", "infoType": "タイプ", "infoId": "ID", "infoResolution": "解像度", @@ -75,8 +87,13 @@ "scalingFit": "フィット", "scalingStretch": "ストレッチ", "scalingDefault": "既定", + "clampClamp": "クランプ", + "clampBorder": "境界", + "clampRepeat": "繰り返し", + "propertyLabelThemeColor": "テーマカラー", "targetAllDisplays": "対象: すべてのディスプレイ", "targetSingleDisplay": "対象: {screen}", + "applySingleDisplay": "{screen} に適用", "filterAll": "すべて", "filterTypeAll": "すべて", "filterTypeScene": "シーン", @@ -84,6 +101,8 @@ "filterTypeWeb": "ウェブ", "filterTypeApplication": "アプリケーション", "filterResAll": "解像度: すべて", + "filterRes4k": "解像度: 4K", + "filterRes8k": "解像度: 8K", "filterResUnknown": "解像度: 不明", "filterStatic": "静的", "filterDynamic": "動的", @@ -100,18 +119,36 @@ "typeApplication": "アプリケーション", "resolutionUnknown": "不明", "dynamicBadge": "動的", + "staticBadge": "静的", + "prevPage": "前へ", + "nextPage": "次へ", + "pageSummary": "{current} / {total} ページ", + "pageRange": "{start}-{end} / {total}", "count": "壁紙数: {count}", - "apply": "適用", - "empty": "ソースディレクトリにフォルダーが見つかりません" + "emptyAll": "ソースディレクトリに壁紙が見つかりません", + "emptyFiltered": "条件に一致する壁紙が見つかりません" }, "settings": { "category": { + "interfaceTitle": "インターフェース", "performanceTitle": "パフォーマンス", - "compatibilityTitle": "互換性", + "compatibilityTitle": "リソース", "audioTitle": "音声", "displayTitle": "表示" }, -"wallpapersFolder": { + "iconColor": { + "label": "アイコンの色", + "description": "バーウィジェットのアイコンに使う色" + }, + "enableExtraPropertiesEditor": { + "label": "追加プロパティ編集を有効化", + "description": "利用可能な場合、壁紙ごとの追加プロパティをパネルのサイドバーに表示します" + }, + "units": { + "fps": " FPS", + "percent": " %" + }, + "wallpapersFolder": { "label": "壁紙ソースディレクトリ", "description": "壁紙プロジェクトフォルダーを含むディレクトリ", "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", @@ -123,9 +160,13 @@ "placeholder": "~/.local/share/wallpaperengine/assets" }, "defaultScaling": { - "label": "既定の拡大縮小", + "label": "既定の拡大縮小モード", "description": "カスタム値が未設定のときに使う既定モード" }, + "defaultClamp": { + "label": "既定のクランプモード", + "description": "壁紙を描画するときに使う既定の境界処理モード" + }, "defaultFps": { "label": "既定 FPS", "description": "壁紙で使う既定のフレームレート上限", @@ -144,11 +185,15 @@ }, "defaultMuted": { "label": "壁紙を既定でミュート", - "description": "壁紙を既定でミュート起動" + "description": "壁紙を既定でミュート状態で起動します" }, "defaultAudioReactiveEffects": { "label": "オーディオ反応効果" }, + "defaultNoAutomute": { + "label": "他のアプリが音声再生中でもミュートしない", + "description": "有効にすると、他のアプリが音を出していても壁紙音声を再生し続けます" + }, "defaultDisableMouse": { "label": "マウス操作を無効化" }, diff --git a/linux-wallpaperengine-controller/i18n/ru.json b/linux-wallpaperengine-controller/i18n/ru.json new file mode 100644 index 00000000..ce4ce423 --- /dev/null +++ b/linux-wallpaperengine-controller/i18n/ru.json @@ -0,0 +1,208 @@ +{ + "widget": { + "tooltip": { + "checking": "Проверка linux-wallpaperengine", + "unavailable": "linux-wallpaperengine не установлен", + "running": "Движок обоев применяет настройки", + "ready": "Открыть панель движка обоев" + } + }, + "menu": { + "reload": "Перезагрузить движок обоев", + "start": "Запустить движок обоев", + "stop": "Остановить движок обоев", + "settings": "Настройки плагина" + }, + "toast": { + "reloaded": "Движок обоев перезагружен", + "stopped": "Движок обоев остановлен", + "reloadSkippedNoWallpaper": "Обои не настроены, поэтому перезагружать нечего" + }, + "main": { + "status": { + "unavailable": "Движок недоступен", + "ready": "Готово", + "starting": "Запуск движка", + "stopped": "Остановлено", + "crashed": "Движок неожиданно завершился" + }, + "error": { + "notInstalled": "linux-wallpaperengine не найден в PATH. Проверьте, установлен ли он правильно.", + "assetsMissing": "Ресурсы Wallpaper Engine не найдены. Укажите каталог ресурсов в настройках.", + "noBackground": "Путь к обоям не настроен.", + "opengl": "Не удалось инициализировать OpenGL. Проверьте драйвер и совместимость композитора.", + "autoRecovered": "Автоматически восстановлена последняя рабочая раскладка" + } + }, + "panel": { + "title": "Wallpaper-Engine Выбор обоев", + "statusChecking": "Проверка", + "statusUnavailable": "Недоступно", + "statusRunning": "Работает", + "statusReady": "Настроено", + "statusStopped": "Остановлено", + "sectionAudio": "Отображение и аудио", + "sectionFeatures": "Функции", + "installHint": "Пример установки: yay -S linux-wallpaperengine-git", + "errorBannerTitle": "Обнаружена ошибка выполнения", + "errorShowDetails": "Показать подробности", + "errorHideDetails": "Скрыть подробности", + "errorDismiss": "Закрыть", + "reload": "Перезагрузить", + "start": "Запустить", + "stop": "Остановить", + "confirm": "Подтвердить", + "cancel": "Отмена", + "compatibilityQuickCheck": "Быстрая проверка совместимости", + "compatibilityQuickCheckRunning": "Проверка совместимости обоев...", + "compatibilityQuickCheckConfirm": "Просканировать все обои на совместимость дополнительных свойств? Это может занять некоторое время.", + "compatibilityQuickCheckFinished": "Проверено обоев: {total}, могут работать некорректно: {failed}", + "folderInvalid": "Каталог источника обоев недействителен или недоступен.", + "scanning": "Сканирование обоев...", + "searchPlaceholder": "Поиск обоев по названию или ID", + "searchClear": "Очистить поиск", + "filterButtonSummary": "Фильтр · {type}", + "sortButtonSummary": "Сортировка · {direction} {sort}", + "applyAllDisplays": "Применить ко всем дисплеям", + "closePanel": "Закрыть панель", + "confirmApply": "Применить обои", + "wallpaperScaling": "Масштабирование", + "wallpaperClamp": "Режим зажатия", + "wallpaperVolume": "Громкость", + "wallpaperMuted": "Без звука", + "wallpaperAudioReactive": "Эффекты аудиореакции", + "wallpaperDisableMouse": "Отключить взаимодействие мышью", + "wallpaperDisableParallax": "Отключить эффект параллакса", + "sectionProperties": "Дополнительные свойства", + "loadingProperties": "Загрузка свойств обоев...", + "propertiesLoadFailed": "Не удалось загрузить свойства обоев. Возможно, формат свойств этих обоев пока не поддерживается, либо сами обои не могут быть корректно загружены и запущены", + "propertiesFailedBadge": "Может не работать", + "noEditableProperties": "У этих обоев пока нет поддерживаемых редактируемых дополнительных свойств", + "propertiesNotice": "Здесь показаны только свойства, которые этот плагин умеет редактировать напрямую", + "infoType": "Тип", + "infoId": "ID", + "infoResolution": "Разрешение", + "infoSize": "Размер", + "scalingFill": "Заполнение", + "scalingFit": "По размеру", + "scalingStretch": "Растянуть", + "scalingDefault": "По умолчанию", + "clampClamp": "Зажатие", + "clampBorder": "Граница", + "clampRepeat": "Повтор", + "propertyLabelThemeColor": "Цвет темы", + "targetAllDisplays": "Цель: все дисплеи", + "targetSingleDisplay": "Цель: {screen}", + "applySingleDisplay": "Применить к {screen}", + "filterAll": "Все", + "filterTypeAll": "Все", + "filterTypeScene": "Сцена", + "filterTypeVideo": "Видео", + "filterTypeWeb": "Веб", + "filterTypeApplication": "Приложение", + "filterResAll": "Разрешение: все", + "filterRes4k": "Разрешение: 4K", + "filterRes8k": "Разрешение: 8K", + "filterResUnknown": "Разрешение: неизвестно", + "filterStatic": "Статические", + "filterDynamic": "Динамические", + "sortName": "Имя", + "sortDateAdded": "Дата добавления", + "sortSize": "Размер", + "sortRecent": "Недавние", + "sortAscendingToggle": "Переключить возр./убыв.", + "sortAscendingToggleWithDirection": "{direction} переключить возр./убыв.", + "sortId": "ID", + "typeScene": "Сцена", + "typeVideo": "Видео", + "typeWeb": "Веб", + "typeApplication": "Приложение", + "resolutionUnknown": "Неизвестно", + "dynamicBadge": "Динамические", + "staticBadge": "Статические", + "prevPage": "Назад", + "nextPage": "Вперёд", + "pageSummary": "Страница {current} / {total}", + "pageRange": "{start}-{end} / {total}", + "count": "Количество обоев: {count}", + "emptyAll": "В исходном каталоге не найдено ни одних обоев", + "emptyFiltered": "Не найдено обоев, соответствующих текущим фильтрам" + }, + "settings": { + "category": { + "interfaceTitle": "Интерфейс", + "performanceTitle": "Производительность", + "compatibilityTitle": "Ресурсы", + "audioTitle": "Аудио", + "displayTitle": "Отображение" + }, + "iconColor": { + "label": "Цвет значка", + "description": "Цвет значка виджета на панели" + }, + "enableExtraPropertiesEditor": { + "label": "Включить редактор дополнительных свойств", + "description": "Показывать в боковой панели дополнительные свойства конкретных обоев, если они доступны" + }, + "units": { + "fps": " FPS", + "percent": " %" + }, + "wallpapersFolder": { + "label": "Каталог источника обоев", + "description": "Каталог, содержащий папки проектов обоев", + "placeholder": "~/.local/share/Steam/steamapps/workshop/content/431960", + "scan": "Автоматически определить каталог мастерской Steam" + }, + "assetsDir": { + "label": "Каталог ресурсов Wallpaper Engine", + "description": "Необязательно: путь к assets, используемый linux-wallpaperengine", + "placeholder": "~/.local/share/wallpaperengine/assets" + }, + "defaultScaling": { + "label": "Режим масштабирования по умолчанию", + "description": "Режим масштабирования по умолчанию, если пользовательское значение не задано" + }, + "defaultClamp": { + "label": "Режим зажатия по умолчанию", + "description": "Способ обработки границ, используемый по умолчанию при рендеринге обоев" + }, + "defaultFps": { + "label": "FPS по умолчанию", + "description": "Ограничение частоты кадров для обоев по умолчанию", + "placeholder": "30" + }, + "defaultNoFullscreenPause": { + "label": "Не приостанавливать в полноэкранном режиме", + "description": "Не приостанавливать обои во время работы полноэкранных приложений" + }, + "defaultFullscreenPauseOnlyActive": { + "label": "Приостанавливать только для активного полноэкранного окна", + "description": "(Только Wayland) Приостанавливать только когда активно полноэкранное окно" + }, + "defaultVolume": { + "label": "Громкость обоев по умолчанию" + }, + "defaultMuted": { + "label": "Отключать звук обоев по умолчанию", + "description": "Запускать обои в беззвучном режиме по умолчанию" + }, + "defaultAudioReactiveEffects": { + "label": "Эффекты аудиореакции" + }, + "defaultNoAutomute": { + "label": "Не заглушать при воспроизведении звука другими приложениями", + "description": "Если включено, звук обоев продолжит воспроизводиться даже когда другие приложения выводят аудио" + }, + "defaultDisableMouse": { + "label": "Отключить взаимодействие мышью" + }, + "defaultDisableParallax": { + "label": "Отключить эффект параллакса" + }, + "autoApplyOnStartup": { + "label": "Автоприменение при запуске", + "description": "Если хотя бы для одного экрана настроены обои, автоматически запускать движок при старте" + } + } +} diff --git a/linux-wallpaperengine-controller/i18n/zh-CN.json b/linux-wallpaperengine-controller/i18n/zh-CN.json index 2afd4534..5d4a173e 100644 --- a/linux-wallpaperengine-controller/i18n/zh-CN.json +++ b/linux-wallpaperengine-controller/i18n/zh-CN.json @@ -9,9 +9,15 @@ }, "menu": { "reload": "重新加载壁纸引擎", + "start": "启动壁纸引擎", "stop": "停止壁纸引擎", "settings": "插件设置" }, + "toast": { + "reloaded": "壁纸引擎已重新加载", + "stopped": "壁纸引擎已停止", + "reloadSkippedNoWallpaper": "当前未配置壁纸,因此无需重新加载" + }, "main": { "status": { "unavailable": "引擎不可用", @@ -29,44 +35,50 @@ } }, "panel": { - "title": "Linux-WallpaperEngine 设置", - "sectionInfo": "信息", - "sectionAudio": "音频与显示", + "title": "Wallpaper-Engine 壁纸选择器", + "statusChecking": "检查中", + "statusUnavailable": "不可用", + "statusRunning": "运行中", + "statusReady": "已配置", + "statusStopped": "已停止", + "sectionAudio": "显示与音频", "sectionFeatures": "功能", "installHint": "安装示例:yay -S linux-wallpaperengine-git", "errorBannerTitle": "检测到运行时错误", "errorShowDetails": "查看详情", "errorHideDetails": "收起详情", "errorDismiss": "关闭提示", - "screen": { - "label": "目标屏幕", - "description": "选择要应用当前壁纸的显示器" - }, "reload": "重载", + "start": "启动", "stop": "停止", + "confirm": "确认", + "cancel": "取消", + "compatibilityQuickCheck": "兼容性快速检查", + "compatibilityQuickCheckRunning": "正在检查壁纸兼容性...", + "compatibilityQuickCheckConfirm": "是否扫描全部壁纸的额外属性兼容性?这可能需要一些时间。", + "compatibilityQuickCheckFinished": "共扫描 {total} 个壁纸,其中 {failed} 个可能异常", "folderInvalid": "壁纸源目录无效或不可访问。", "scanning": "正在扫描壁纸...", - "searchPlaceholder": "搜索壁纸...", + "searchPlaceholder": "搜索壁纸,可使用名称和 ID 搜索", "searchClear": "清空搜索", - "filterButton": "筛选", "filterButtonSummary": "筛选 · {type}", - "sortButton": "排序", "sortButtonSummary": "排序 · {direction}{sort}", "applyAllDisplays": "应用到全部显示器", - "perDisplayMode": "多显示器", - "switchToAllDisplays": "应用在全部显示器上", - "switchToPerDisplay": "多显示器分别应用", "closePanel": "关闭面板", - "confirmApply": "确认应用", - "cancelSelection": "取消", - "pendingHint": "已选中但未应用,可先调整设置后再确认。", + "confirmApply": "应用壁纸", "wallpaperScaling": "缩放", + "wallpaperClamp": "边界模式", "wallpaperVolume": "音量", "wallpaperMuted": "静音", - "wallpaperAudioReactive": "音频响应", + "wallpaperAudioReactive": "音频响应效果", "wallpaperDisableMouse": "禁用鼠标交互", "wallpaperDisableParallax": "禁用视差效果", - "resetWallpaperSettings": "重置为全局默认", + "sectionProperties": "额外属性", + "loadingProperties": "正在读取壁纸属性...", + "propertiesLoadFailed": "读取壁纸属性失败,可能是该壁纸的属性格式暂未支持,或该壁纸本身无法被正确加载和运行", + "propertiesFailedBadge": "可能异常", + "noEditableProperties": "此壁纸暂时没有可编辑的额外属性", + "propertiesNotice": "这里只显示当前插件可直接编辑的属性", "infoType": "类型", "infoId": "ID", "infoResolution": "分辨率", @@ -75,8 +87,13 @@ "scalingFit": "适应", "scalingStretch": "拉伸", "scalingDefault": "默认", + "clampClamp": "钳制", + "clampBorder": "边框", + "clampRepeat": "重复", + "propertyLabelThemeColor": "主题色", "targetAllDisplays": "目标:全部显示器", "targetSingleDisplay": "目标:{screen}", + "applySingleDisplay": "应用到 {screen}", "filterAll": "全部", "filterTypeAll": "全部", "filterTypeScene": "场景", @@ -84,6 +101,8 @@ "filterTypeWeb": "网页", "filterTypeApplication": "应用", "filterResAll": "分辨率:全部", + "filterRes4k": "分辨率:4K", + "filterRes8k": "分辨率:8K", "filterResUnknown": "分辨率:未知", "filterStatic": "静态", "filterDynamic": "动态", @@ -100,17 +119,35 @@ "typeApplication": "应用", "resolutionUnknown": "未知", "dynamicBadge": "动态", + "staticBadge": "静态", + "prevPage": "上一页", + "nextPage": "下一页", + "pageSummary": "第 {current} / {total} 页", + "pageRange": "{start}-{end} / 共 {total} 项", "count": "壁纸数量:{count}", - "apply": "应用", - "empty": "源目录中未找到任何文件夹" + "emptyAll": "源目录中未找到任何壁纸", + "emptyFiltered": "源目录中未找到符合条件的壁纸" }, "settings": { "category": { + "interfaceTitle": "界面", "performanceTitle": "性能", - "compatibilityTitle": "兼容性", + "compatibilityTitle": "资源", "audioTitle": "音频", "displayTitle": "显示" }, + "iconColor": { + "label": "图标颜色", + "description": "用于栏组件图标的颜色" + }, + "enableExtraPropertiesEditor": { + "label": "启用额外属性编辑器", + "description": "在可用时于面板侧栏显示壁纸专属的额外属性" + }, + "units": { + "fps": " FPS", + "percent": " %" + }, "wallpapersFolder": { "label": "壁纸源目录", "description": "包含壁纸项目文件夹的目录", @@ -123,9 +160,13 @@ "placeholder": "~/.local/share/wallpaperengine/assets" }, "defaultScaling": { - "label": "默认缩放", + "label": "默认缩放模式", "description": "未设置自定义值时的默认缩放模式" }, + "defaultClamp": { + "label": "默认边界模式", + "description": "渲染壁纸时默认使用的边界处理方式" + }, "defaultFps": { "label": "默认 FPS", "description": "壁纸默认使用的帧率上限", @@ -149,6 +190,10 @@ "defaultAudioReactiveEffects": { "label": "音频响应效果" }, + "defaultNoAutomute": { + "label": "其他应用播放音频时不静音", + "description": "开启后,其他应用输出声音时壁纸也会继续播放音频" + }, "defaultDisableMouse": { "label": "禁用鼠标交互" }, diff --git a/linux-wallpaperengine-controller/i18n/zh-TW.json b/linux-wallpaperengine-controller/i18n/zh-TW.json index 70fd2299..2520910d 100644 --- a/linux-wallpaperengine-controller/i18n/zh-TW.json +++ b/linux-wallpaperengine-controller/i18n/zh-TW.json @@ -9,9 +9,15 @@ }, "menu": { "reload": "重新載入壁紙引擎", + "start": "啟動壁紙引擎", "stop": "停止壁紙引擎", "settings": "外掛設定" }, + "toast": { + "reloaded": "壁紙引擎已重新載入", + "stopped": "壁紙引擎已停止", + "reloadSkippedNoWallpaper": "目前未設定壁紙,因此無需重新載入" + }, "main": { "status": { "unavailable": "引擎不可用", @@ -29,44 +35,50 @@ } }, "panel": { - "title": "Linux-WallpaperEngine 設定", - "sectionInfo": "資訊", - "sectionAudio": "音訊與顯示", + "title": "Wallpaper-Engine 壁紙選擇器", + "statusChecking": "檢查中", + "statusUnavailable": "不可用", + "statusRunning": "執行中", + "statusReady": "已配置", + "statusStopped": "已停止", + "sectionAudio": "顯示與音訊", "sectionFeatures": "功能", "installHint": "安裝範例:yay -S linux-wallpaperengine-git", "errorBannerTitle": "偵測到執行期錯誤", "errorShowDetails": "查看詳情", "errorHideDetails": "收起詳情", "errorDismiss": "關閉提示", - "screen": { - "label": "目標螢幕", - "description": "選擇要套用目前壁紙的顯示器" - }, - "reload": "重載", + "reload": "重新載入", + "start": "啟動", "stop": "停止", + "confirm": "確認", + "cancel": "取消", + "compatibilityQuickCheck": "相容性快速檢查", + "compatibilityQuickCheckRunning": "正在檢查壁紙相容性...", + "compatibilityQuickCheckConfirm": "是否掃描全部壁紙的額外屬性相容性?這可能需要一些時間。", + "compatibilityQuickCheckFinished": "共掃描 {total} 個壁紙,其中 {failed} 個可能異常", "folderInvalid": "壁紙來源目錄無效或不可存取。", "scanning": "正在掃描壁紙...", - "searchPlaceholder": "搜尋壁紙...", + "searchPlaceholder": "搜尋壁紙,可使用名稱和 ID 搜尋", "searchClear": "清除搜尋", - "filterButton": "篩選", "filterButtonSummary": "篩選 · {type}", - "sortButton": "排序", "sortButtonSummary": "排序 · {direction}{sort}", "applyAllDisplays": "套用到全部顯示器", - "perDisplayMode": "多顯示器", - "switchToAllDisplays": "套用在全部顯示器上", - "switchToPerDisplay": "多顯示器分別套用", "closePanel": "關閉面板", - "confirmApply": "確認套用", - "cancelSelection": "取消", - "pendingHint": "已選取但尚未套用,可先調整設定後再確認。", + "confirmApply": "套用壁紙", "wallpaperScaling": "縮放", + "wallpaperClamp": "邊界模式", "wallpaperVolume": "音量", "wallpaperMuted": "靜音", - "wallpaperAudioReactive": "音訊響應", + "wallpaperAudioReactive": "音訊響應效果", "wallpaperDisableMouse": "停用滑鼠互動", "wallpaperDisableParallax": "停用視差效果", - "resetWallpaperSettings": "重設為全域預設", + "sectionProperties": "額外屬性", + "loadingProperties": "正在讀取壁紙屬性...", + "propertiesLoadFailed": "讀取壁紙屬性失敗,可能是該壁紙的屬性格式尚未支援,或該壁紙本身無法被正確載入與執行", + "propertiesFailedBadge": "可能異常", + "noEditableProperties": "此壁紙目前沒有可編輯的額外屬性", + "propertiesNotice": "這裡只顯示目前外掛可直接編輯的屬性", "infoType": "類型", "infoId": "ID", "infoResolution": "解析度", @@ -75,8 +87,13 @@ "scalingFit": "適應", "scalingStretch": "拉伸", "scalingDefault": "預設", + "clampClamp": "鉗制", + "clampBorder": "邊框", + "clampRepeat": "重複", + "propertyLabelThemeColor": "主題色", "targetAllDisplays": "目標:全部顯示器", "targetSingleDisplay": "目標:{screen}", + "applySingleDisplay": "套用到 {screen}", "filterAll": "全部", "filterTypeAll": "全部", "filterTypeScene": "場景", @@ -84,6 +101,8 @@ "filterTypeWeb": "網頁", "filterTypeApplication": "應用", "filterResAll": "解析度:全部", + "filterRes4k": "解析度:4K", + "filterRes8k": "解析度:8K", "filterResUnknown": "解析度:未知", "filterStatic": "靜態", "filterDynamic": "動態", @@ -100,17 +119,35 @@ "typeApplication": "應用", "resolutionUnknown": "未知", "dynamicBadge": "動態", + "staticBadge": "靜態", + "prevPage": "上一頁", + "nextPage": "下一頁", + "pageSummary": "第 {current} / {total} 頁", + "pageRange": "{start}-{end} / 共 {total} 項", "count": "壁紙數量:{count}", - "apply": "套用", - "empty": "來源目錄中找不到任何資料夾" + "emptyAll": "來源目錄中找不到任何壁紙", + "emptyFiltered": "來源目錄中找不到符合條件的壁紙" }, "settings": { "category": { + "interfaceTitle": "介面", "performanceTitle": "效能", - "compatibilityTitle": "相容性", + "compatibilityTitle": "資源", "audioTitle": "音訊", "displayTitle": "顯示" }, + "iconColor": { + "label": "圖示顏色", + "description": "用於欄元件圖示的顏色" + }, + "enableExtraPropertiesEditor": { + "label": "啟用額外屬性編輯器", + "description": "在可用時於面板側欄顯示壁紙專屬的額外屬性" + }, + "units": { + "fps": " FPS", + "percent": " %" + }, "wallpapersFolder": { "label": "壁紙來源目錄", "description": "包含壁紙項目資料夾的目錄", @@ -123,9 +160,13 @@ "placeholder": "~/.local/share/wallpaperengine/assets" }, "defaultScaling": { - "label": "預設縮放", + "label": "預設縮放模式", "description": "未設定自訂值時使用的預設縮放模式" }, + "defaultClamp": { + "label": "預設邊界模式", + "description": "渲染壁紙時預設使用的邊界處理方式" + }, "defaultFps": { "label": "預設 FPS", "description": "壁紙預設使用的幀率上限", @@ -149,6 +190,10 @@ "defaultAudioReactiveEffects": { "label": "音訊響應效果" }, + "defaultNoAutomute": { + "label": "其他應用播放音訊時不靜音", + "description": "開啟後,其他應用輸出聲音時壁紙也會繼續播放音訊" + }, "defaultDisableMouse": { "label": "停用滑鼠互動" }, diff --git a/linux-wallpaperengine-controller/manifest.json b/linux-wallpaperengine-controller/manifest.json index d00d87b3..57977941 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.0.6", + "version": "1.1.2", "minNoctaliaVersion": "4.6.6", "author": "PaloMiku", "license": "MIT", @@ -25,11 +25,15 @@ "defaultSettings": { "wallpapersFolder": "~/.local/share/Steam/steamapps/workshop/content/431960", "assetsDir": "", + "iconColor": "none", + "enableExtraPropertiesEditor": true, "defaultScaling": "fill", + "defaultClamp": "clamp", "defaultFps": 30, "defaultVolume": 100, "defaultMuted": true, "defaultAudioReactiveEffects": true, + "defaultNoAutomute": false, "defaultDisableMouse": false, "defaultDisableParallax": false, "defaultNoFullscreenPause": false, @@ -37,6 +41,7 @@ "autoApplyOnStartup": true, "panelLastSelectedPath": "", "screens": {}, + "wallpaperProperties": {}, "lastKnownGoodScreens": {}, "runtimeRecoveryPending": false } diff --git a/linux-wallpaperengine-controller/preview.png b/linux-wallpaperengine-controller/preview.png index d5bd3477..640dbb7a 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 new file mode 100644 index 00000000..889dbdbf --- /dev/null +++ b/linux-wallpaperengine-controller/scripts/scan-properties-compatibility.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +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 + status=0 + else + status=1 + fi + + printf '%s\t%s\n' "$wallpaper_dir" "$status" +done