mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
1684 lines
57 KiB
QML
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");
|
|
}
|
|
}
|
|
}
|