Files
noctalia-shell/Services/UI/WallpaperService.qml
T

1684 lines
57 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Power
import qs.Services.Theming
import qs.Services.UI
Singleton {
id: root
readonly property ListModel fillModeModel: ListModel {}
readonly property string defaultDirectory: Settings.preprocessPath(Settings.data.wallpaper.directory)
readonly property string solidColorPrefix: "solid://"
// 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
// Cache for current wallpapers - can be updated directly since we use signals for notifications
property var currentWallpapers: ({})
// Track current alphabetical index for each screen
property var alphabeticalIndices: ({})
// Track used wallpapers for random mode (persisted across reboots)
property var usedRandomWallpapers: ({})
property bool isInitialized: false
property string wallpaperCacheFile: ""
readonly property bool scanning: (scanningCount > 0)
readonly property string noctaliaDefaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
property string defaultWallpaper: noctaliaDefaultWallpaper
// Signals for reactive UI updates
signal wallpaperChanged(string screenName, string path)
// Emitted when a wallpaper changes
signal wallpaperProcessingComplete(string screenName, string path, string cachedPath)
// Emitted when wallpaper processing (resize/cache) is complete. cachedPath is the resized version.
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
// Browse mode: track current browse path per screen (separate from root directory)
property var currentBrowsePaths: ({})
// Wallpaper panel: which appearance slot (light/dark) new selections apply to — like picking a monitor tab
property string wallpaperSelectionAppearance: "light"
// Bumped when favorites are added/removed so grid delegates can refresh star state
property int favoritesRevision: 0
// After favoriting, refresh snapshot once theme colors finish transitioning
property var pendingFavoriteSchemeRefresh: null
// Signal emitted when browse path changes for a screen
signal browsePathChanged(string screenName, string path)
Timer {
id: favoriteSchemeDebounceTimer
interval: 450
repeat: false
property string pendingPath: ""
property string pendingSlot: ""
onTriggered: {
var p = pendingPath;
var s = pendingSlot;
pendingPath = "";
pendingSlot = "";
if (p && root.isFavorite(p)) {
root.updateFavoriteColorScheme(p, s);
}
}
}
function scheduleFavoriteSchemeSnapshot(path, slot) {
Qt.callLater(function () {
if (root.isFavorite(path)) {
root.updateFavoriteColorScheme(path, slot);
}
});
favoriteSchemeDebounceTimer.pendingPath = path;
favoriteSchemeDebounceTimer.pendingSlot = slot;
favoriteSchemeDebounceTimer.restart();
if (Color.isTransitioning) {
root.pendingFavoriteSchemeRefresh = {
"path": path,
"slot": slot
};
}
}
Connections {
target: Settings.data.wallpaper
function onDirectoryChanged() {
root.usedRandomWallpapers = {};
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.usedRandomWallpapers = {};
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 onAutomationEnabledChanged() {
root.toggleRandomWallpaper();
}
function onRandomIntervalSecChanged() {
root.restartRandomWallpaperTimer();
}
function onWallpaperChangeModeChanged() {
// Reset alphabetical indices when mode changes
root.alphabeticalIndices = {};
if (Settings.data.wallpaper.automationEnabled) {
root.restartRandomWallpaperTimer();
root.setNextWallpaper();
}
}
function onViewModeChanged() {
// Reset browse paths to root when mode changes
root.currentBrowsePaths = {};
root.refreshWallpapersList();
}
function onShowHiddenFilesChanged() {
root.refreshWallpapersList();
}
function onUseSolidColorChanged() {
if (Settings.data.wallpaper.useSolidColor) {
var solidPath = root.createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
for (var i = 0; i < Quickshell.screens.length; i++) {
root.wallpaperChanged(Quickshell.screens[i].name, solidPath);
}
} else {
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name;
root.wallpaperChanged(screenName, root.getWallpaper(screenName) || root.defaultWallpaper);
}
}
}
function onSolidColorChanged() {
if (Settings.data.wallpaper.useSolidColor) {
var solidPath = root.createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
for (var i = 0; i < Quickshell.screens.length; i++) {
root.wallpaperChanged(Quickshell.screens[i].name, solidPath);
}
}
}
function onSortOrderChanged() {
root.refreshWallpapersList();
}
function onLinkLightAndDarkWallpapersChanged() {
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light";
root._syncWallpaperSlotsWhenLinking();
}
root._notifyAllWallpapersChanged();
}
}
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light";
}
// Restore scheme from favorite for this light/dark slot before wallpaper refresh
root.reapplyFavoriteThemeForActiveWallpaper();
root._notifyAllWallpapersChanged();
}
}
Connections {
target: WallhavenService
function onWallpaperDownloaded() {
root.refreshWallpapersList();
}
}
// -------------------------------------------------
function init() {
Logger.i("Wallpaper", "Service started");
translateModels();
Qt.callLater(root._dedupeWallpaperFavoritesByPath);
// Initialize cache file path
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
wallpaperCacheFile = Settings.cacheDir + "wallpapers.json";
wallpaperCacheView.path = wallpaperCacheFile;
}
});
// Note: isInitialized will be set to true in wallpaperCacheView.onLoaded
Logger.d("Wallpaper", "Triggering initial wallpaper scan");
Qt.callLater(refreshWallpapersList);
}
// Cache restore updates currentWallpapers without _setWallpaper, so wallpaperChanged does not fire.
function _scheduleThemeSyncFromCachedWallpaper() {
Qt.callLater(function () {
root.reapplyFavoriteThemeForActiveWallpaper();
if (Settings.data.colorSchemes.useWallpaperColors) {
AppThemeService.generate();
}
});
}
// -------------------------------------------------
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("positions.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
});
fillModeModel.append({
"key": "repeat",
"name": I18n.tr("wallpaper.fill-modes.repeat"),
"uniform": 4.0
});
// Populate transitionsModel with translated names
transitionsModel.append({
"key": "none",
"name": I18n.tr("common.none")
});
transitionsModel.append({
"key": "random",
"name": I18n.tr("common.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")
});
transitionsModel.append({
"key": "pixelate",
"name": I18n.tr("wallpaper.transitions.pixelate")
});
transitionsModel.append({
"key": "honeycomb",
"name": I18n.tr("wallpaper.transitions.honeycomb")
});
}
// -------------------------------------------------------------------
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;
}
// -------------------------------------------------------------------
// Solid color helpers
// -------------------------------------------------------------------
function isSolidColorPath(path) {
return path && typeof path === "string" && path.startsWith(solidColorPrefix);
}
function getSolidColor(path) {
if (!isSolidColorPath(path)) {
return null;
}
return path.substring(solidColorPrefix.length);
}
function createSolidColorPath(colorString) {
return solidColorPrefix + colorString;
}
function setSolidColor(colorString) {
Settings.data.wallpaper.solidColor = colorString;
Settings.data.wallpaper.useSolidColor = true;
}
// -------------------------------------------------------------------
// Per-screen wallpaper: persisted as { light, dark } (legacy string loads are normalized)
// -------------------------------------------------------------------
function _isSplitWallpaperEntry(entry) {
if (!entry || typeof entry !== "object") {
return false;
}
return entry.light !== undefined || entry.dark !== undefined;
}
function _pathsFromEntry(entry) {
if (!entry) {
return {
light: "",
dark: ""
};
}
if (typeof entry === "string") {
return {
light: entry,
dark: entry
};
}
return {
light: entry.light || "",
dark: entry.dark || ""
};
}
function _cloneWallpaperEntry(entry) {
if (typeof entry === "string") {
return {
light: entry,
dark: entry
};
}
var p = _pathsFromEntry(entry);
return {
light: p.light,
dark: p.dark
};
}
function _entriesEqual(a, b) {
if (a === b) {
return true;
}
if (typeof a === "string" && typeof b === "string") {
return a === b;
}
if (typeof a === "string" || typeof b === "string") {
return false;
}
if (!_isSplitWallpaperEntry(a) || !_isSplitWallpaperEntry(b)) {
return false;
}
var pa = _pathsFromEntry(a);
var pb = _pathsFromEntry(b);
return pa.light === pb.light && pa.dark === pb.dark;
}
function _entryToEffectivePath(entry) {
if (!entry) {
return "";
}
if (typeof entry === "string") {
return entry;
}
var p = _pathsFromEntry(entry);
if (Settings.data.colorSchemes.darkMode) {
return p.dark || p.light || "";
}
return p.light || p.dark || "";
}
function _normalizeAppearanceSlot(slot) {
return slot === "dark" ? "dark" : "light";
}
function _defaultAppearanceSlotForChange(slot) {
if (slot === "light" || slot === "dark") {
return slot;
}
return Settings.data.colorSchemes.darkMode ? "dark" : "light";
}
function getWallpaperPathForSlot(screenName, appearanceSlot) {
if (Settings.data.wallpaper.useSolidColor) {
return createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
}
var slot = _normalizeAppearanceSlot(appearanceSlot);
var entry = currentWallpapers[screenName];
if (!entry) {
return "";
}
var p = _pathsFromEntry(entry);
if (slot === "dark") {
return p.dark || p.light || "";
}
return p.light || p.dark || "";
}
function getWallpapersEffectiveMap() {
var out = {};
for (var i = 0; i < Quickshell.screens.length; i++) {
var n = Quickshell.screens[i].name;
out[n] = getWallpaper(n);
}
return out;
}
function _ensureObjectWallpaperEntries() {
var names = {};
Object.keys(currentWallpapers).forEach(function (k) {
names[k] = true;
});
for (var i = 0; i < Quickshell.screens.length; i++) {
names[Quickshell.screens[i].name] = true;
}
Object.keys(names).forEach(function (name) {
var e = currentWallpapers[name];
if (!e) {
return;
}
if (typeof e === "string") {
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
currentWallpapers[name] = {
light: e,
dark: e
};
} else {
currentWallpapers[name] = {
light: e,
dark: e
};
}
} else if (_isSplitWallpaperEntry(e)) {
var p = _pathsFromEntry(e);
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
currentWallpapers[name] = {
light: p.light || p.dark || "",
dark: p.dark || p.light || ""
};
} else {
currentWallpapers[name] = {
light: p.light || "",
dark: p.dark || ""
};
}
}
});
saveTimer.restart();
}
function _notifyAllWallpapersChanged() {
for (var i = 0; i < Quickshell.screens.length; i++) {
var n = Quickshell.screens[i].name;
root.wallpaperChanged(n, getWallpaper(n));
}
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper data
function getMonitorConfig(screenName) {
var monitors = Settings.data.wallpaper.monitorDirectories;
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.monitorDirectories || [];
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.monitorDirectories = newMonitors.slice();
root.wallpaperDirectoryChanged(screenName, Settings.preprocessPath(directory));
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper - now from cache
function getWallpaper(screenName) {
// Return solid color path when in solid color mode
if (Settings.data.wallpaper.useSolidColor) {
return createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
}
var entry = currentWallpapers[screenName];
if (entry) {
var effective = _entryToEffectivePath(entry);
if (effective) {
return effective;
}
}
// Try to inherit wallpaper from another active screen
var inherited = _inheritWallpaperFromExistingScreen(screenName);
if (inherited) {
return inherited;
}
return root.defaultWallpaper;
}
// -------------------------------------------------------------------
function changeWallpaper(path, screenName, appearanceSlot) {
// Turn off solid color mode when selecting a wallpaper
if (Settings.data.wallpaper.useSolidColor) {
Settings.data.wallpaper.useSolidColor = false;
}
var slot = _defaultAppearanceSlotForChange(appearanceSlot);
// Save current favorite color schemes before switching away.
// This must happen before applyFavoriteTheme (called by the UI)
// overwrites the settings that _createFavoriteEntry reads.
_saveOutgoingFavorites(path, screenName, slot);
if (screenName !== undefined) {
_setWallpaper(screenName, path, slot);
} else {
var allScreenNames = new Set(Object.keys(currentWallpapers));
for (var i = 0; i < Quickshell.screens.length; i++) {
allScreenNames.add(Quickshell.screens[i].name);
}
allScreenNames.forEach(name => _setWallpaper(name, path, slot));
}
}
// -------------------------------------------------------------------
// Save the color scheme of any favorited wallpapers that are about
// to be replaced, while the current settings still reflect them.
function _saveOutgoingFavorites(newPath, screenName, appearanceSlot) {
var paths = [];
var slot = _normalizeAppearanceSlot(appearanceSlot);
function collectFromEntry(e) {
if (!e) {
return;
}
if (typeof e === "string") {
if (e && e !== newPath) {
paths.push(e);
}
return;
}
if (!_isSplitWallpaperEntry(e)) {
return;
}
var p = _pathsFromEntry(e);
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
if (p.light && p.light !== newPath) {
paths.push(p.light);
}
if (p.dark && p.dark !== newPath && p.dark !== p.light) {
paths.push(p.dark);
}
} else {
var old = slot === "dark" ? p.dark : p.light;
if (old && old !== newPath) {
paths.push(old);
}
}
}
if (screenName !== undefined) {
collectFromEntry(currentWallpapers[screenName]);
} else {
var names = new Set(Object.keys(currentWallpapers));
for (var i = 0; i < Quickshell.screens.length; i++) {
names.add(Quickshell.screens[i].name);
}
names.forEach(function (name) {
collectFromEntry(currentWallpapers[name]);
});
}
var unique = [];
for (var j = 0; j < paths.length; j++) {
if (unique.indexOf(paths[j]) === -1) {
unique.push(paths[j]);
}
}
unique.forEach(function (path) {
if (!path || path === newPath) {
return;
}
var favIdx = _findAnyFavoriteIndexForPath(path);
if (favIdx === _favoriteNotFound) {
return;
}
var app = _favoriteAppearanceSlot(Settings.data.wallpaper.favorites[favIdx]);
updateFavoriteColorScheme(path, app);
});
}
// -------------------------------------------------------------------
function _inheritWallpaperFromExistingScreen(screenName) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var otherName = Quickshell.screens[i].name;
if (otherName === screenName) {
continue;
}
var entry = currentWallpapers[otherName];
if (!entry) {
continue;
}
var cloned = _cloneWallpaperEntry(entry);
if (_entriesEqual(currentWallpapers[screenName], cloned)) {
return _entryToEffectivePath(cloned);
}
currentWallpapers[screenName] = cloned;
saveTimer.restart();
root.wallpaperChanged(screenName, _entryToEffectivePath(cloned));
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart();
}
return _entryToEffectivePath(cloned);
}
return "";
}
function _syncWallpaperSlotsWhenLinking() {
var names = new Set(Object.keys(currentWallpapers));
for (var i = 0; i < Quickshell.screens.length; i++) {
names.add(Quickshell.screens[i].name);
}
names.forEach(function (name) {
var e = currentWallpapers[name];
if (!e) {
return;
}
var eff = _entryToEffectivePath(e);
if (!eff) {
return;
}
var merged = {
light: eff,
dark: eff
};
if (_entriesEqual(e, merged)) {
return;
}
currentWallpapers[name] = merged;
saveTimer.restart();
root.wallpaperChanged(name, eff);
});
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart();
}
}
function _setWallpaper(screenName, path, appearanceSlot) {
if (path === "" || path === undefined) {
return;
}
if (screenName === undefined) {
Logger.w("Wallpaper", "setWallpaper", "no screen specified");
return;
}
var slot = _normalizeAppearanceSlot(appearanceSlot);
var oldEntry = currentWallpapers[screenName];
var p = _pathsFromEntry(oldEntry);
var newEntry;
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
newEntry = {
light: path,
dark: path
};
} else if (slot === "dark") {
newEntry = {
light: p.light || "",
dark: path
};
} else {
newEntry = {
light: path,
dark: p.dark || ""
};
}
if (_entriesEqual(oldEntry, newEntry)) {
return;
}
currentWallpapers[screenName] = newEntry;
saveTimer.restart();
root.wallpaperChanged(screenName, _entryToEffectivePath(newEntry));
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart();
}
}
// -------------------------------------------------------------------
function setRandomWallpaper(screen) {
Logger.d("Wallpaper", "setRandomWallpaper");
if (Settings.data.wallpaper.enableMultiMonitorDirectories) {
if (screen === undefined) {
// 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 randomPath = _pickUnusedRandom(screenName, wallpaperList);
changeWallpaper(randomPath, screenName);
}
}
} else {
// Pick a random wallpaper for the specified screen
var wallpaperList = getWallpapersList(screen);
if (wallpaperList.length > 0) {
var randomPath = _pickUnusedRandom(screen, wallpaperList);
changeWallpaper(randomPath, screen);
}
}
} 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 randomPath = _pickUnusedRandom("all", wallpaperList);
changeWallpaper(randomPath, screen);
}
}
}
// -------------------------------------------------------------------
// Pick a random wallpaper that hasn't been used yet in the current cycle.
// Once all wallpapers have been shown, resets the pool (keeping only the
// last-shown wallpaper to avoid an immediate repeat).
function _pickUnusedRandom(key, wallpaperList) {
var used = usedRandomWallpapers[key] || [];
// Clean stale entries (files that were removed from the directory)
var wallpaperSet = new Set(wallpaperList);
used = used.filter(function (path) {
return wallpaperSet.has(path);
});
// Filter to wallpapers that haven't been used yet
var unused = wallpaperList.filter(function (path) {
return used.indexOf(path) === -1;
});
// If all have been used, reset but keep the last one to avoid immediate repeat
if (unused.length === 0) {
var lastUsed = used.length > 0 ? used[used.length - 1] : "";
used = lastUsed ? [lastUsed] : [];
unused = wallpaperList.filter(function (path) {
return used.indexOf(path) === -1;
});
// Edge case: only one wallpaper in the directory
if (unused.length === 0) {
unused = wallpaperList;
}
Logger.d("Wallpaper", "All wallpapers used for", key, "- resetting pool");
}
// Pick randomly from unused
var randomIndex = Math.floor(Math.random() * unused.length);
var picked = unused[randomIndex];
// Record as used
used.push(picked);
usedRandomWallpapers[key] = used;
// Persist
saveTimer.restart();
return picked;
}
// -------------------------------------------------------------------
function setAlphabeticalWallpaper() {
Logger.d("Wallpaper", "setAlphabeticalWallpaper");
if (Settings.data.wallpaper.enableMultiMonitorDirectories) {
// Pick next alphabetical 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) {
// Get or initialize index for this screen
if (alphabeticalIndices[screenName] === undefined) {
// Find current wallpaper in list to set initial index
var currentWallpaper = getWallpaper(screenName) || "";
var foundIndex = wallpaperList.indexOf(currentWallpaper);
alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0;
}
// Get next index (wrap around)
var currentIndex = alphabeticalIndices[screenName];
var nextIndex = (currentIndex + 1) % wallpaperList.length;
alphabeticalIndices[screenName] = nextIndex;
var nextPath = wallpaperList[nextIndex];
changeWallpaper(nextPath, screenName);
}
}
} else {
// Pick next alphabetical wallpaper common to all screens
var wallpaperList = getWallpapersList(Screen.name);
if (wallpaperList.length > 0) {
// Use primary screen name as key for single directory mode
var key = "all";
if (alphabeticalIndices[key] === undefined) {
// Find current wallpaper in list to set initial index
var currentWallpaper = getWallpaper(Screen.name) || "";
var foundIndex = wallpaperList.indexOf(currentWallpaper);
alphabeticalIndices[key] = (foundIndex >= 0) ? foundIndex : 0;
}
// Get next index (wrap around)
var currentIndex = alphabeticalIndices[key];
var nextIndex = (currentIndex + 1) % wallpaperList.length;
alphabeticalIndices[key] = nextIndex;
var nextPath = wallpaperList[nextIndex];
changeWallpaper(nextPath, undefined);
}
}
}
// -------------------------------------------------------------------
function toggleRandomWallpaper() {
Logger.d("Wallpaper", "toggleRandomWallpaper");
if (Settings.data.wallpaper.automationEnabled) {
restartRandomWallpaperTimer();
setNextWallpaper();
}
}
// -------------------------------------------------------------------
function setNextWallpaper() {
var mode = Settings.data.wallpaper.wallpaperChangeMode || "random";
if (mode === "alphabetical") {
setAlphabeticalWallpaper();
} else {
setRandomWallpaper();
}
}
// -------------------------------------------------------------------
function restartRandomWallpaperTimer() {
if (Settings.data.wallpaper.automationEnabled) {
randomWallpaperTimer.restart();
}
}
// -------------------------------------------------------------------
function getWallpapersList(screenName) {
if (screenName != undefined && wallpaperLists[screenName] != undefined) {
return wallpaperLists[screenName];
}
return [];
}
// -------------------------------------------------------------------
// Browse mode helper functions
// -------------------------------------------------------------------
function getCurrentBrowsePath(screenName) {
if (currentBrowsePaths[screenName] !== undefined) {
var stored = currentBrowsePaths[screenName];
var root = getMonitorDirectory(screenName);
if (root && stored.startsWith(root)) {
return stored;
}
// Stored path is outside the root directory, reset it
delete currentBrowsePaths[screenName];
}
return getMonitorDirectory(screenName);
}
function setBrowsePath(screenName, path) {
if (!screenName)
return;
currentBrowsePaths[screenName] = path;
browsePathChanged(screenName, path);
}
function navigateUp(screenName) {
if (!screenName)
return;
var currentPath = getCurrentBrowsePath(screenName);
var rootPath = getMonitorDirectory(screenName);
// Don't navigate if root is invalid or we're already at root
if (!rootPath || currentPath === rootPath)
return;
// Get parent directory
var parentPath = currentPath.replace(/\/[^\/]+\/?$/, "");
if (parentPath === "")
parentPath = rootPath;
// Don't go above root
if (!parentPath.startsWith(rootPath)) {
parentPath = rootPath;
}
setBrowsePath(screenName, parentPath);
}
function navigateToRoot(screenName) {
if (!screenName)
return;
var rootPath = getMonitorDirectory(screenName);
setBrowsePath(screenName, rootPath);
}
// Scan directory with optional directory listing (for browse mode)
// callback receives { files: [], directories: [] }
function scanDirectoryWithDirs(screenName, directory, callback) {
if (!directory || directory === "") {
callback({
files: [],
directories: []
});
return;
}
var result = {
files: [],
directories: []
};
var pendingScans = 2;
function checkComplete() {
pendingScans--;
if (pendingScans === 0) {
// Files are already sorted by _scanDirectoryInternal according to sortOrder setting
// Only sort directories alphabetically
result.directories.sort();
callback(result);
}
}
// Scan for files
_scanDirectoryInternal(screenName, directory, false, false, function (files) {
result.files = files;
checkComplete();
});
// Scan for directories
_scanForDirectories(directory, function (dirs) {
result.directories = dirs;
checkComplete();
});
}
function _scanForDirectories(directory, callback) {
var findArgs = ["find", "-L", directory, "-maxdepth", "1", "-mindepth", "1", "-type", "d"];
var processString = `
import QtQuick
import Quickshell.Io
Process {
id: process
command: ${JSON.stringify(findArgs)}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
`;
var processObject = Qt.createQmlObject(processString, root, "DirScan");
processObject.exited.connect(function (exitCode) {
var dirs = [];
if (exitCode === 0) {
var lines = processObject.stdout.text.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
var showHidden = Settings.data.wallpaper.showHiddenFiles;
var name = line.split('/').pop();
if (showHidden || !name.startsWith('.')) {
dirs.push(line);
}
}
}
}
callback(dirs);
processObject.destroy();
});
processObject.running = true;
}
// -------------------------------------------------------------------
function refreshWallpapersList() {
// Wait for imageMagickAvailable to be correctly set for ImageCacheService.imageFilters
if (!ImageCacheService.initialized) {
Qt.callLater(refreshWallpapersList);
return;
}
var mode = Settings.data.wallpaper.viewMode;
Logger.d("Wallpaper", "refreshWallpapersList", "viewMode:", mode);
scanningCount = 0;
if (mode === "recursive") {
// 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 if (mode === "browse") {
// Browse mode: scan current browse path (non-recursive)
// Note: The actual directory+subdirectory scanning happens in WallpaperPanel
// Here we just scan the current browse path for files
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name;
var directory = getCurrentBrowsePath(screenName);
_scanDirectoryInternal(screenName, directory, false, true, null);
}
} else {
// Single directory mode (non-recursive)
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name;
var directory = getMonitorDirectory(screenName);
_scanDirectoryInternal(screenName, directory, false, true, null);
}
}
}
// Internal scan function
// recursive: whether to scan subdirectories
// updateList: whether to update wallpaperLists and emit signal
// callback: optional callback with files array
function _scanDirectoryInternal(screenName, directory, recursive, updateList, callback) {
if (!directory || directory === "") {
Logger.w("Wallpaper", "Empty directory for", screenName);
if (updateList) {
wallpaperLists[screenName] = [];
wallpaperListChanged(screenName, 0);
}
if (callback)
callback([]);
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];
if (updateList)
scanningCount--;
}
if (updateList)
scanningCount++;
Logger.i("Wallpaper", "Starting scan for", screenName, "in", directory, "recursive:", recursive);
// Build find command args dynamically from ImageCacheService filters
var filters = ImageCacheService.imageFilters;
var findArgs = ["find", "-L", directory];
// Add depth limit for non-recursive
if (!recursive) {
findArgs.push("-maxdepth", "1", "-mindepth", "1");
}
findArgs.push("-type", "f", "(");
for (var i = 0; i < filters.length; i++) {
if (i > 0) {
findArgs.push("-o");
}
findArgs.push("-iname");
findArgs.push(filters[i]);
}
findArgs.push(")");
// Add printf to get modification time
findArgs.push("-printf", "%T@|%p\\n");
// Create Process component inline
var processString = `
import QtQuick
import Quickshell.Io
Process {
id: process
command: ${JSON.stringify(findArgs)}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
`;
var processObject = Qt.createQmlObject(processString, root, "Scan_" + screenName);
// Store reference to avoid garbage collection
if (updateList) {
recursiveProcesses[screenName] = processObject;
}
var handler = function (exitCode) {
if (updateList)
scanningCount--;
Logger.d("Wallpaper", "Process exited with code", exitCode, "for", screenName);
var files = [];
if (exitCode === 0) {
var lines = processObject.stdout.text.split('\n');
var parsedFiles = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
var parts = line.split('|');
if (parts.length >= 2) { // Handle potential extra pipes in filename by joining rest
var timestamp = parseFloat(parts[0]);
var path = parts.slice(1).join('|');
var showHidden = Settings.data.wallpaper.showHiddenFiles;
var name = path.split('/').pop();
if (showHidden || !name.startsWith('.')) {
parsedFiles.push({
path: path,
time: timestamp,
name: name
});
}
} else if (line.indexOf('|') === -1) {
// Fallback for unexpected output format or old find versions (unlikely but safe)
var path = line;
var showHidden = Settings.data.wallpaper.showHiddenFiles;
var name = path.split('/').pop();
if (showHidden || !name.startsWith('.')) {
parsedFiles.push({
path: path,
time: 0,
name: name
});
}
}
}
}
// Sort files based on settings
var sortOrder = Settings.data.wallpaper.sortOrder || "name";
// Fischer-Yates shuffle
if (sortOrder === "random") {
for (let i = parsedFiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = parsedFiles[i];
parsedFiles[i] = parsedFiles[j];
parsedFiles[j] = temp;
}
} else {
parsedFiles.sort(function (a, b) {
if (sortOrder === "date_desc") { // Newest first
return b.time - a.time;
} else if (sortOrder === "date_asc") { // Oldest first
return a.time - b.time;
} else if (sortOrder === "name_desc") {
return b.name.localeCompare(a.name);
} else { // name (asc)
return a.name.localeCompare(b.name);
}
});
}
// Map back to string array
files = parsedFiles.map(f => f.path);
if (updateList) {
wallpaperLists[screenName] = files;
// Reset alphabetical indices when list changes
if (alphabeticalIndices[screenName] !== undefined) {
var currentWallpaper = getWallpaper(screenName) || "";
var foundIndex = files.indexOf(currentWallpaper);
alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0;
}
Logger.i("Wallpaper", "Scan completed for", screenName, "found", files.length, "files");
wallpaperListChanged(screenName, files.length);
}
} else {
Logger.w("Wallpaper", "Scan failed for", screenName, "exit code:", exitCode, "(directory might not exist)");
if (updateList) {
wallpaperLists[screenName] = [];
if (alphabeticalIndices[screenName] !== undefined) {
alphabeticalIndices[screenName] = 0;
}
wallpaperListChanged(screenName, 0);
}
}
// Clean up
if (updateList) {
delete recursiveProcesses[screenName];
}
if (callback)
callback(files);
processObject.destroy();
};
processObject.exited.connect(handler);
Logger.d("Wallpaper", "Starting process for", screenName);
processObject.running = true;
}
// Process instances for scanning (one per screen)
property var recursiveProcesses: ({})
// -------------------------------------------------------------------
function scanDirectoryRecursive(screenName, directory) {
_scanDirectoryInternal(screenName, directory, true, true, null);
}
// -------------------------------------------------------------------
// Favorites
// -------------------------------------------------------------------
// TODO (~few weeks): Remove per-favorite `darkMode` (the boolean on each
// Settings.data.wallpaper.favorites[] entry). It duplicates `appearance` and is
// unrelated to Settings.data.colorSchemes.darkMode (global shell light/dark).
// Plan: one-time migration, then drop writes and the fallback in _favoriteAppearanceSlot.
// -------------------------------------------------------------------
readonly property int _favoriteNotFound: -1
// -------------------------------------------------------------------
function _favoriteAppearanceSlot(f) {
if (f.appearance === "light" || f.appearance === "dark") {
return f.appearance;
}
return f.darkMode ? "dark" : "light";
}
function _findFavoriteIndex(path, appearanceSlot) {
var favorites = Settings.data.wallpaper.favorites;
var searchPath = Settings.preprocessPath(path);
var slot = _normalizeAppearanceSlot(appearanceSlot);
for (var i = 0; i < favorites.length; i++) {
if (Settings.preprocessPath(favorites[i].path) !== searchPath) {
continue;
}
if (_favoriteAppearanceSlot(favorites[i]) === slot) {
return i;
}
}
return _favoriteNotFound;
}
function _findAnyFavoriteIndexForPath(path) {
var favorites = Settings.data.wallpaper.favorites;
var searchPath = Settings.preprocessPath(path);
for (var i = 0; i < favorites.length; i++) {
if (Settings.preprocessPath(favorites[i].path) === searchPath) {
return i;
}
}
return _favoriteNotFound;
}
function _dedupeWallpaperFavoritesByPath() {
var favorites = Settings.data.wallpaper.favorites;
if (!favorites || !favorites.length) {
return;
}
var seen = {};
var out = [];
for (var i = 0; i < favorites.length; i++) {
var key = Settings.preprocessPath(favorites[i].path);
if (!key || seen[key]) {
continue;
}
seen[key] = true;
out.push(favorites[i]);
}
if (out.length !== favorites.length) {
Settings.data.wallpaper.favorites = out;
root.favoritesRevision++;
}
}
// -------------------------------------------------------------------
function _createFavoriteEntry(path, appearanceSlot) {
var app = _normalizeAppearanceSlot(appearanceSlot);
return {
"path": path,
"appearance": app,
"colorScheme": Settings.data.colorSchemes.predefinedScheme,
"darkMode": app === "dark" // TODO: remove per-favorite field (see Favorites section note)
,
"useWallpaperColors": Settings.data.colorSchemes.useWallpaperColors,
"generationMethod": Settings.data.colorSchemes.generationMethod,
"paletteColors": [Color.mPrimary.toString(), Color.mSecondary.toString(), Color.mTertiary.toString(), Color.mError.toString()]
};
}
// -------------------------------------------------------------------
// Favorites are per (path, light|dark): at most one entry per path, tagged with the tab you starred from.
function isFavorite(path, appearanceSlot) {
if (appearanceSlot === undefined || appearanceSlot === null || appearanceSlot === "") {
return _findAnyFavoriteIndexForPath(path) !== _favoriteNotFound;
}
return _findFavoriteIndex(path, appearanceSlot) !== _favoriteNotFound;
}
// Single favorite entry per path; use _favoriteAppearanceSlot(entry) for light vs dark it was starred under.
function favoriteEntryForPath(path) {
var idx = _findAnyFavoriteIndexForPath(path);
if (idx === _favoriteNotFound) {
return null;
}
return Settings.data.wallpaper.favorites[idx];
}
function getFavoriteForDisplay(path) {
return favoriteEntryForPath(path);
}
// -------------------------------------------------------------------
function toggleFavorite(path, appearanceSlot, screenName) {
var slot;
if (appearanceSlot !== undefined && appearanceSlot !== null && appearanceSlot !== "") {
slot = _normalizeAppearanceSlot(appearanceSlot);
} else {
slot = _normalizeAppearanceSlot(root.wallpaperSelectionAppearance);
}
var favorites = Settings.data.wallpaper.favorites.slice();
var anyIdx = _findAnyFavoriteIndexForPath(path);
var applyWallpaperForSlot = false;
if (anyIdx !== _favoriteNotFound) {
var existingSlot = _favoriteAppearanceSlot(favorites[anyIdx]);
if (existingSlot === slot) {
favorites.splice(anyIdx, 1);
Logger.d("Wallpaper", "Removed favorite:", path, slot);
if (favoriteSchemeDebounceTimer.pendingPath === path && favoriteSchemeDebounceTimer.pendingSlot === slot) {
favoriteSchemeDebounceTimer.stop();
favoriteSchemeDebounceTimer.pendingPath = "";
favoriteSchemeDebounceTimer.pendingSlot = "";
}
if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path && root.pendingFavoriteSchemeRefresh.slot === slot) {
root.pendingFavoriteSchemeRefresh = null;
}
} else if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
favorites[anyIdx] = _createFavoriteEntry(path, slot);
Logger.d("Wallpaper", "Moved favorite to other appearance:", path, slot);
root.scheduleFavoriteSchemeSnapshot(path, slot);
applyWallpaperForSlot = true;
if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path) {
root.pendingFavoriteSchemeRefresh = {
"path": path,
"slot": slot
};
}
} else {
// Separate light/dark wallpapers: star is remove-only (no sun/moon hint for "move" vs unfavorite).
favorites.splice(anyIdx, 1);
Logger.d("Wallpaper", "Removed favorite (star on other appearance tab, separate wallpapers):", path);
if (favoriteSchemeDebounceTimer.pendingPath === path) {
favoriteSchemeDebounceTimer.stop();
favoriteSchemeDebounceTimer.pendingPath = "";
favoriteSchemeDebounceTimer.pendingSlot = "";
}
if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path) {
root.pendingFavoriteSchemeRefresh = null;
}
}
} else {
favorites.push(_createFavoriteEntry(path, slot));
Logger.d("Wallpaper", "Added favorite:", path, slot);
root.scheduleFavoriteSchemeSnapshot(path, slot);
applyWallpaperForSlot = true;
}
Settings.data.wallpaper.favorites = favorites;
root.favoritesRevision++;
if (applyWallpaperForSlot) {
var scr;
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
scr = undefined;
} else if (screenName !== undefined && screenName !== null && screenName !== "") {
scr = screenName;
} else if (Quickshell.screens.length > 0) {
scr = Quickshell.screens[0].name;
} else {
scr = undefined;
}
root.changeWallpaper(path, scr, slot);
root.applyFavoriteTheme(path, scr, slot);
}
favoritesChanged(path);
}
// Apply saved scheme from a favorite. Optional appearanceSlotOverride sets light vs dark target (UI tab or system mode).
function _applyFavoriteThemeFromEntry(favorite, appearanceSlotOverride) {
if (!favorite) {
return;
}
var favApp;
if (appearanceSlotOverride !== undefined && appearanceSlotOverride !== null && appearanceSlotOverride !== "") {
favApp = _normalizeAppearanceSlot(appearanceSlotOverride);
} else {
favApp = _favoriteAppearanceSlot(favorite);
}
var targetDark = favApp === "dark";
var generationMethodChanging = Settings.data.colorSchemes.generationMethod !== favorite.generationMethod;
var darkModeChanging = Settings.data.colorSchemes.darkMode !== targetDark;
var useWallpaperColorsChanging = Settings.data.colorSchemes.useWallpaperColors !== favorite.useWallpaperColors;
Settings.data.colorSchemes.useWallpaperColors = favorite.useWallpaperColors;
Settings.data.colorSchemes.predefinedScheme = favorite.colorScheme;
Settings.data.colorSchemes.generationMethod = favorite.generationMethod;
Settings.data.colorSchemes.darkMode = targetDark;
// If nothing triggered AppThemeService via change handlers, regenerate once.
if (!generationMethodChanging && !darkModeChanging && !useWallpaperColorsChanging) {
AppThemeService.generate();
} else if (!generationMethodChanging && !darkModeChanging && useWallpaperColorsChanging) {
AppThemeService.generate();
}
}
// When light/dark changes (or on startup), re-load scheme from the favorite for the wallpaper now shown for that slot.
function reapplyFavoriteThemeForActiveWallpaper() {
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : "";
}
var wp = getWallpaper(effectiveMonitor);
if (!wp || isSolidColorPath(wp)) {
return;
}
var slot = Settings.data.colorSchemes.darkMode ? "dark" : "light";
var favorite = favoriteEntryForPath(wp);
if (!favorite) {
return;
}
_applyFavoriteThemeFromEntry(favorite, slot);
}
// -------------------------------------------------------------------
function applyFavoriteTheme(path, screenName, appearanceSlot) {
// Only apply theme if the wallpaper is on the monitor driving colors
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : "";
}
if (screenName !== undefined && screenName !== effectiveMonitor) {
return;
}
var slot;
if (appearanceSlot !== undefined && appearanceSlot !== null && appearanceSlot !== "") {
slot = _normalizeAppearanceSlot(appearanceSlot);
} else {
slot = _normalizeAppearanceSlot(root.wallpaperSelectionAppearance);
}
var favorite = favoriteEntryForPath(path);
if (!favorite) {
return;
}
var schemeSlot = slot;
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
schemeSlot = _favoriteAppearanceSlot(favorite);
}
_applyFavoriteThemeFromEntry(favorite, schemeSlot);
}
// -------------------------------------------------------------------
function updateFavoriteColorScheme(path, appearanceSlot) {
var slot = _normalizeAppearanceSlot(appearanceSlot);
var existingIndex = _findFavoriteIndex(path, slot);
if (existingIndex === _favoriteNotFound) {
return;
}
var favorites = Settings.data.wallpaper.favorites.slice();
favorites[existingIndex] = _createFavoriteEntry(favorites[existingIndex].path, slot);
Settings.data.wallpaper.favorites = favorites;
Logger.d("Wallpaper", "Updated color scheme for favorite:", path, slot);
favoriteDataUpdated(path);
}
signal favoritesChanged(string path)
signal favoriteDataUpdated(string path)
// Auto-update favorite palette colors when theme colors finish transitioning
Connections {
target: Color
function onIsTransitioningChanged() {
if (!Color.isTransitioning) {
_updateCurrentWallpaperFavorites();
}
}
}
function _updateCurrentWallpaperFavorites() {
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : "";
}
var wp = getWallpaper(effectiveMonitor);
if (!wp) {
return;
}
var favIdx = _findAnyFavoriteIndexForPath(wp);
if (favIdx === _favoriteNotFound) {
return;
}
var app = _favoriteAppearanceSlot(Settings.data.wallpaper.favorites[favIdx]);
updateFavoriteColorScheme(wp, app);
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// -------------------------------------------------------------------
Timer {
id: randomWallpaperTimer
interval: Settings.data.wallpaper.randomIntervalSec * 1000
running: Settings.data.wallpaper.automationEnabled && !PowerProfileService.noctaliaPerformanceMode
repeat: true
onTriggered: setNextWallpaper()
triggeredOnStart: false
}
// -------------------------------------------------------------------
// Cache file persistence
// -------------------------------------------------------------------
FileView {
id: wallpaperCacheView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: wallpaperCacheAdapter
property var wallpapers: ({})
property string defaultWallpaper: root.noctaliaDefaultWallpaper
property var usedRandomWallpapers: ({})
}
onLoaded: {
// Load wallpapers from cache file
root.currentWallpapers = wallpaperCacheAdapter.wallpapers || {};
root.usedRandomWallpapers = wallpaperCacheAdapter.usedRandomWallpapers || {};
root._ensureObjectWallpaperEntries();
if (Settings.data.wallpaper.linkLightAndDarkWallpapers) {
root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light";
root._syncWallpaperSlotsWhenLinking();
}
// Load default wallpaper from cache if it exists, otherwise use Noctalia default
if (wallpaperCacheAdapter.defaultWallpaper && wallpaperCacheAdapter.defaultWallpaper !== "") {
root.defaultWallpaper = wallpaperCacheAdapter.defaultWallpaper;
Logger.d("Wallpaper", "Loaded default wallpaper from cache:", wallpaperCacheAdapter.defaultWallpaper);
} else {
root.defaultWallpaper = root.noctaliaDefaultWallpaper;
Logger.d("Wallpaper", "Using Noctalia default wallpaper");
}
Logger.d("Wallpaper", "Loaded wallpapers from cache file:", Object.keys(root.currentWallpapers).length, "screens");
root.isInitialized = true;
root._scheduleThemeSyncFromCachedWallpaper();
}
onLoadFailed: error => {
// File doesn't exist yet or failed to load - initialize with empty state
root.currentWallpapers = {};
Logger.d("Wallpaper", "Cache file doesn't exist or failed to load, starting with empty wallpapers");
root.isInitialized = true;
root._scheduleThemeSyncFromCachedWallpaper();
}
}
Timer {
id: saveTimer
interval: 500
repeat: false
onTriggered: {
wallpaperCacheAdapter.wallpapers = root.currentWallpapers;
wallpaperCacheAdapter.defaultWallpaper = root.defaultWallpaper;
wallpaperCacheAdapter.usedRandomWallpapers = root.usedRandomWallpapers;
wallpaperCacheView.writeAdapter();
Logger.d("Wallpaper", "Saved wallpapers to cache file");
}
}
}