pragma Singleton import Qt.labs.folderlistmodel import QtQuick import Quickshell import Quickshell.Io import qs.Commons Singleton { id: root readonly property ListModel fillModeModel: ListModel {} readonly property string defaultDirectory: Settings.preprocessPath(Settings.data.wallpaper.directory) // All available wallpaper transitions readonly property ListModel transitionsModel: ListModel {} // All transition keys but filter out "none" and "random" so we are left with the real transitions readonly property var allTransitions: Array.from({ "length": transitionsModel.count }, (_, i) => transitionsModel.get(i).key).filter(key => key !== "random" && key != "none") property var wallpaperLists: ({}) property int scanningCount: 0 readonly property bool scanning: (scanningCount > 0) // Cache for current wallpapers - can be updated directly since we use signals for notifications property var currentWallpapers: ({}) property bool isInitialized: false // Signals for reactive UI updates signal wallpaperChanged(string screenName, string path) // Emitted when a wallpaper changes signal wallpaperDirectoryChanged(string screenName, string directory) // Emitted when a monitor's directory changes signal wallpaperListChanged(string screenName, int count) // Emitted when available wallpapers list changes Connections { target: Settings.data.wallpaper function onDirectoryChanged() { root.refreshWallpapersList(); // Emit directory change signals for monitors using the default directory if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { // All monitors use the main directory for (var i = 0; i < Quickshell.screens.length; i++) { root.wallpaperDirectoryChanged(Quickshell.screens[i].name, root.defaultDirectory); } } else { // Only monitors without custom directories are affected for (var i = 0; i < Quickshell.screens.length; i++) { var screenName = Quickshell.screens[i].name; var monitor = root.getMonitorConfig(screenName); if (!monitor || !monitor.directory) { root.wallpaperDirectoryChanged(screenName, root.defaultDirectory); } } } } function onEnableMultiMonitorDirectoriesChanged() { root.refreshWallpapersList(); // Notify all monitors about potential directory changes for (var i = 0; i < Quickshell.screens.length; i++) { var screenName = Quickshell.screens[i].name; root.wallpaperDirectoryChanged(screenName, root.getMonitorDirectory(screenName)); } } function onRandomEnabledChanged() { root.toggleRandomWallpaper(); } function onRandomIntervalSecChanged() { root.restartRandomWallpaperTimer(); } function onRecursiveSearchChanged() { root.refreshWallpapersList(); } } // ------------------------------------------------- function init() { Logger.i("Wallpaper", "Service started"); translateModels(); // Load wallpapers from ShellState first (faster), then fall back to Settings currentWallpapers = ({}); if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { var cachedWallpapers = ShellState.getWallpapers(); if (cachedWallpapers && Object.keys(cachedWallpapers).length > 0) { currentWallpapers = cachedWallpapers; Logger.d("Wallpaper", "Loaded wallpapers from ShellState"); } else { // Fall back to Settings if ShellState is empty loadFromSettings(); } } else { // ShellState not ready yet, load from Settings loadFromSettings(); } isInitialized = true; Logger.d("Wallpaper", "Triggering initial wallpaper scan"); Qt.callLater(refreshWallpapersList); } function loadFromSettings() { var monitors = Settings.data.wallpaper.monitors || []; for (var i = 0; i < monitors.length; i++) { if (monitors[i].name && monitors[i].wallpaper) { currentWallpapers[monitors[i].name] = monitors[i].wallpaper; } } Logger.d("Wallpaper", "Loaded wallpapers from Settings"); // Migrate to ShellState if we loaded from Settings if (typeof ShellState !== 'undefined' && ShellState.isLoaded && Object.keys(currentWallpapers).length > 0) { ShellState.setWallpapers(currentWallpapers); Logger.i("Wallpaper", "Migrated wallpaper paths from Settings to ShellState"); } } // ------------------------------------------------- function translateModels() { // Wait for i18n to be ready by retrying every time if (!I18n.isLoaded) { Qt.callLater(translateModels); return; } // Populate fillModeModel with translated names fillModeModel.append({ "key": "center", "name": I18n.tr("wallpaper.fill-modes.center"), "uniform": 0.0 }); fillModeModel.append({ "key": "crop", "name": I18n.tr("wallpaper.fill-modes.crop"), "uniform": 1.0 }); fillModeModel.append({ "key": "fit", "name": I18n.tr("wallpaper.fill-modes.fit"), "uniform": 2.0 }); fillModeModel.append({ "key": "stretch", "name": I18n.tr("wallpaper.fill-modes.stretch"), "uniform": 3.0 }); // Populate transitionsModel with translated names transitionsModel.append({ "key": "none", "name": I18n.tr("wallpaper.transitions.none") }); transitionsModel.append({ "key": "random", "name": I18n.tr("wallpaper.transitions.random") }); transitionsModel.append({ "key": "fade", "name": I18n.tr("wallpaper.transitions.fade") }); transitionsModel.append({ "key": "disc", "name": I18n.tr("wallpaper.transitions.disc") }); transitionsModel.append({ "key": "stripes", "name": I18n.tr("wallpaper.transitions.stripes") }); transitionsModel.append({ "key": "wipe", "name": I18n.tr("wallpaper.transitions.wipe") }); } // ------------------------------------------------------------------- function getFillModeUniform() { for (var i = 0; i < fillModeModel.count; i++) { const mode = fillModeModel.get(i); if (mode.key === Settings.data.wallpaper.fillMode) { return mode.uniform; } } // Fallback to crop return 1.0; } // ------------------------------------------------------------------- // Get specific monitor wallpaper data function getMonitorConfig(screenName) { var monitors = Settings.data.wallpaper.monitors; if (monitors !== undefined) { for (var i = 0; i < monitors.length; i++) { if (monitors[i].name !== undefined && monitors[i].name === screenName) { return monitors[i]; } } } } // ------------------------------------------------------------------- // Get specific monitor directory function getMonitorDirectory(screenName) { if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { return root.defaultDirectory; } var monitor = getMonitorConfig(screenName); if (monitor !== undefined && monitor.directory !== undefined) { return Settings.preprocessPath(monitor.directory); } // Fall back to the main/single directory return root.defaultDirectory; } // ------------------------------------------------------------------- // Set specific monitor directory function setMonitorDirectory(screenName, directory) { var monitors = Settings.data.wallpaper.monitors || []; var found = false; // Create a new array with updated values var newMonitors = monitors.map(function (monitor) { if (monitor.name === screenName) { found = true; return { "name": screenName, "directory": directory, "wallpaper": monitor.wallpaper || "" }; } return monitor; }); if (!found) { newMonitors.push({ "name": screenName, "directory": directory, "wallpaper": "" }); } // Update Settings with new array to ensure proper persistence Settings.data.wallpaper.monitors = newMonitors.slice(); root.wallpaperDirectoryChanged(screenName, Settings.preprocessPath(directory)); } // ------------------------------------------------------------------- // Get specific monitor wallpaper - now from cache function getWallpaper(screenName) { return currentWallpapers[screenName] || Settings.data.wallpaper.defaultWallpaper; } // ------------------------------------------------------------------- function changeWallpaper(path, screenName) { if (screenName !== undefined) { _setWallpaper(screenName, path); } else { // If no screenName specified change for all screens for (var i = 0; i < Quickshell.screens.length; i++) { _setWallpaper(Quickshell.screens[i].name, path); } } } // ------------------------------------------------------------------- function _setWallpaper(screenName, path) { if (path === "" || path === undefined) { return; } if (screenName === undefined) { Logger.w("Wallpaper", "setWallpaper", "no screen specified"); return; } //Logger.i("Wallpaper", "setWallpaper on", screenName, ": ", path) // Check if wallpaper actually changed var oldPath = currentWallpapers[screenName] || ""; var wallpaperChanged = (oldPath !== path); if (!wallpaperChanged) { // No change needed return; } // Update cache directly currentWallpapers[screenName] = path; // Save to ShellState (wallpaper paths now only stored here, not in Settings) if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { ShellState.setWallpapers(currentWallpapers); } // Emit signal for this specific wallpaper change root.wallpaperChanged(screenName, path); // Restart the random wallpaper timer if (randomWallpaperTimer.running) { randomWallpaperTimer.restart(); } } // ------------------------------------------------------------------- function setRandomWallpaper() { Logger.d("Wallpaper", "setRandomWallpaper"); if (Settings.data.wallpaper.enableMultiMonitorDirectories) { // Pick a random wallpaper per screen for (var i = 0; i < Quickshell.screens.length; i++) { var screenName = Quickshell.screens[i].name; var wallpaperList = getWallpapersList(screenName); if (wallpaperList.length > 0) { var randomIndex = Math.floor(Math.random() * wallpaperList.length); var randomPath = wallpaperList[randomIndex]; changeWallpaper(randomPath, screenName); } } } else { // Pick a random wallpaper common to all screens // We can use any screenName here, so we just pick the primary one. var wallpaperList = getWallpapersList(Screen.name); if (wallpaperList.length > 0) { var randomIndex = Math.floor(Math.random() * wallpaperList.length); var randomPath = wallpaperList[randomIndex]; changeWallpaper(randomPath, undefined); } } } // ------------------------------------------------------------------- function toggleRandomWallpaper() { Logger.d("Wallpaper", "toggleRandomWallpaper"); if (Settings.data.wallpaper.randomEnabled) { restartRandomWallpaperTimer(); setRandomWallpaper(); } } // ------------------------------------------------------------------- function restartRandomWallpaperTimer() { if (Settings.data.wallpaper.isRandom) { randomWallpaperTimer.restart(); } } // ------------------------------------------------------------------- function getWallpapersList(screenName) { if (screenName != undefined && wallpaperLists[screenName] != undefined) { return wallpaperLists[screenName]; } return []; } // ------------------------------------------------------------------- function refreshWallpapersList() { Logger.d("Wallpaper", "refreshWallpapersList", "recursive:", Settings.data.wallpaper.recursiveSearch); scanningCount = 0; if (Settings.data.wallpaper.recursiveSearch) { // Use Process-based recursive search for all screens for (var i = 0; i < Quickshell.screens.length; i++) { var screenName = Quickshell.screens[i].name; var directory = getMonitorDirectory(screenName); scanDirectoryRecursive(screenName, directory); } } else { // Use FolderListModel (non-recursive) // Force refresh by toggling each scanner's currentDirectory for (var i = 0; i < wallpaperScanners.count; i++) { var scanner = wallpaperScanners.objectAt(i); if (scanner) { // Capture scanner in closure (function (s) { var directory = root.getMonitorDirectory(s.screenName); // Trigger a change by setting to /tmp (always exists) then back to the actual directory // Note: This causes harmless Qt warnings (QTBUG-52262) but is necessary to force FolderListModel to re-scan s.currentDirectory = "/tmp"; Qt.callLater(function () { s.currentDirectory = directory; }); })(scanner); } } } } // Process instances for recursive scanning (one per screen) property var recursiveProcesses: ({}) // ------------------------------------------------------------------- function scanDirectoryRecursive(screenName, directory) { if (!directory || directory === "") { Logger.w("Wallpaper", "Empty directory for", screenName); wallpaperLists[screenName] = []; wallpaperListChanged(screenName, 0); return; } // Cancel any existing scan for this screen if (recursiveProcesses[screenName]) { Logger.d("Wallpaper", "Cancelling existing scan for", screenName); recursiveProcesses[screenName].running = false; recursiveProcesses[screenName].destroy(); delete recursiveProcesses[screenName]; scanningCount--; } scanningCount++; Logger.i("Wallpaper", "Starting recursive scan for", screenName, "in", directory); // Create Process component inline var processComponent = Qt.createComponent("", root); var processString = ` import QtQuick import Quickshell.Io Process { id: process command: ["find", "-L", "` + directory + `", "-type", "f", "(", "-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.gif", "-o", "-iname", "*.pnm", "-o", "-iname", "*.bmp", ")"] stdout: StdioCollector {} stderr: StdioCollector {} } `; var processObject = Qt.createQmlObject(processString, root, "RecursiveScan_" + screenName); // Store reference to avoid garbage collection recursiveProcesses[screenName] = processObject; var handler = function (exitCode) { scanningCount--; Logger.d("Wallpaper", "Process exited with code", exitCode, "for", screenName); if (exitCode === 0) { var lines = processObject.stdout.text.split('\n'); var files = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (line !== '') { files.push(line); } } // Sort files for consistent ordering files.sort(); wallpaperLists[screenName] = files; Logger.i("Wallpaper", "Recursive scan completed for", screenName, "found", files.length, "files"); wallpaperListChanged(screenName, files.length); } else { Logger.w("Wallpaper", "Recursive scan failed for", screenName, "exit code:", exitCode, "(directory might not exist)"); wallpaperLists[screenName] = []; wallpaperListChanged(screenName, 0); } // Clean up delete recursiveProcesses[screenName]; processObject.destroy(); }; processObject.exited.connect(handler); Logger.d("Wallpaper", "Starting process for", screenName); processObject.running = true; } // ------------------------------------------------------------------- // ------------------------------------------------------------------- // ------------------------------------------------------------------- Timer { id: randomWallpaperTimer interval: Settings.data.wallpaper.randomIntervalSec * 1000 running: Settings.data.wallpaper.randomEnabled repeat: true onTriggered: setRandomWallpaper() triggeredOnStart: false } // Instantiator (not Repeater) to create FolderListModel for each monitor Instantiator { id: wallpaperScanners model: Quickshell.screens delegate: FolderListModel { property string screenName: modelData.name property string currentDirectory: root.getMonitorDirectory(screenName) folder: "file://" + currentDirectory nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] showDirs: false sortField: FolderListModel.Name // Watch for directory changes via property binding onCurrentDirectoryChanged: { folder = "file://" + currentDirectory; } Component.onCompleted: { // Connect to directory change signal root.wallpaperDirectoryChanged.connect(function (screen, directory) { if (screen === screenName) { currentDirectory = directory; } }); } onStatusChanged: { if (status === FolderListModel.Null) { // Flush the list root.wallpaperLists[screenName] = []; root.wallpaperListChanged(screenName, 0); } else if (status === FolderListModel.Loading) { // Flush the list root.wallpaperLists[screenName] = []; scanningCount++; } else if (status === FolderListModel.Ready) { var files = []; for (var i = 0; i < count; i++) { var directory = root.getMonitorDirectory(screenName); var filepath = directory + "/" + get(i, "fileName"); files.push(filepath); } // Update the list root.wallpaperLists[screenName] = files; scanningCount--; Logger.d("Wallpaper", "List refreshed for", screenName, "count:", files.length); root.wallpaperListChanged(screenName, files.length); } } } } }