pragma Singleton import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io import qs.Commons Singleton { id: root Component.onCompleted: { Logger.log("Wallpaper", "Service started") // Initialize cache from Settings on startup 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 } } } // All available wallpaper transitions readonly property ListModel transitionsModel: ListModel { ListElement { key: "none" name: "None" } ListElement { key: "random" name: "Random" } ListElement { key: "fade" name: "Fade" } ListElement { key: "disc" name: "Disc" } ListElement { key: "stripes" name: "Stripes" } ListElement { key: "wipe" name: "Wipe" } } readonly property ListModel fillModeModel: ListModel { // Centers image without resizing // Pads with fillColor if image is smaller than screen ListElement { key: "center" name: "Center" uniform: 0.0 } // Scales image to fill entire screen // Crops portions that exceed screen bounds // Maintains aspect ratio ListElement { key: "crop" name: "Crop (Fill/Cover)" uniform: 1.0 } // Scales image to fit entirely within screen // Maintains aspect ratio // May show fillColor bars on sides ListElement { key: "fit" name: "Fit (Contain)" uniform: 2.0 } // Stretches image to exact screen dimensions // Does NOT maintain aspect ratio // May distort the image ListElement { key: "stretch" name: "Stretch" uniform: 3.0 } } 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 } // 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: ({}) // 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, Settings.data.wallpaper.directory) } } 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, Settings.data.wallpaper.directory) } } } } 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() } } // ------------------------------------------------------------------- // 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 Settings.data.wallpaper.directory } var monitor = getMonitorConfig(screenName) if (monitor !== undefined && monitor.directory !== undefined) { return monitor.directory } // Fall back to the main/single directory return Settings.data.wallpaper.directory } // ------------------------------------------------------------------- // 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, directory) } // ------------------------------------------------------------------- // Get specific monitor wallpaper - now from cache function getWallpaper(screenName) { return currentWallpapers[screenName] || "" } // ------------------------------------------------------------------- function changeWallpaper(screenName, path) { 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.warn("Wallpaper", "setWallpaper", "no screen specified") return } //Logger.log("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 // Update Settings - still need immutable update for Settings persistence // The slice() ensures Settings detects the change and saves properly var monitors = Settings.data.wallpaper.monitors || [] var found = false var newMonitors = monitors.map(function (monitor) { if (monitor.name === screenName) { found = true return { "name": screenName, "directory": monitor.directory || getMonitorDirectory(screenName), "wallpaper": path } } return monitor }) if (!found) { newMonitors.push({ "name": screenName, "directory": getMonitorDirectory(screenName), "wallpaper": path }) } Settings.data.wallpaper.monitors = newMonitors.slice() // Emit signal for this specific wallpaper change root.wallpaperChanged(screenName, path) // Restart the random wallpaper timer if (randomWallpaperTimer.running) { randomWallpaperTimer.restart() } } // ------------------------------------------------------------------- function setRandomWallpaper() { Logger.log("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(screenName, randomPath) } } } 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(undefined, randomPath) } } } // ------------------------------------------------------------------- function toggleRandomWallpaper() { Logger.log("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.log("Wallpaper", "refreshWallpapersList") scanningCount = 0 // Force refresh by toggling the folder property on each FolderListModel for (var i = 0; i < wallpaperScanners.count; i++) { var scanner = wallpaperScanners.objectAt(i) if (scanner) { var currentFolder = scanner.folder scanner.folder = "" scanner.folder = currentFolder } } } // ------------------------------------------------------------------- // ------------------------------------------------------------------- // ------------------------------------------------------------------- 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.log("Wallpaper", "List refreshed for", screenName, "count:", files.length) root.wallpaperListChanged(screenName, files.length) } } } } }