wallpaper: ability to manually browse into subfolders, service cleanup, ui improvements

This commit is contained in:
Lemmy
2026-01-22 00:08:12 -05:00
parent 32022eaf58
commit 3a0b20ab8c
8 changed files with 502 additions and 202 deletions
+12 -2
View File
@@ -1370,8 +1370,12 @@
"settings-monitor-specific-description": "Set a different wallpaper folder for each monitor.",
"settings-monitor-specific-label": "Monitor-specific directories",
"settings-monitor-specific-tooltip": "Monitor wallpaper folder",
"settings-recursive-search-description": "Also search for wallpapers in subfolders of the wallpaper directory.",
"settings-recursive-search-label": "Search subfolders",
"settings-view-mode-description": "Choose how wallpapers are displayed from your directory.",
"settings-view-mode-label": "Viewing mode",
"view-mode-browse": "Browse directories",
"view-mode-cycle-tooltip": "View mode: {mode} (click to change)",
"view-mode-recursive": "Flattened subdirectories",
"view-mode-single": "Root directory",
"settings-select-monitor-folder": "Select monitor wallpaper folder",
"settings-selector-description": "Choose your wallpaper.",
"settings-selector-position-description": "Choose where the wallpaper selector panel appears.",
@@ -1604,6 +1608,12 @@
"wallpaper-selector": "Wallpaper selector"
},
"wallpaper": {
"browse": {
"empty-directory": "This directory is empty.",
"go-root": "Go to wallpaper root",
"go-up": "Go to parent folder",
"go-up-hint": "Use the back button to navigate up."
},
"configure-directory": "Configure your wallpaper directory with images.",
"fill-modes": {
"crop": "Crop (Fill)",
+1 -1
View File
@@ -139,7 +139,7 @@
"directory": "",
"monitorDirectories": [],
"enableMultiMonitorDirectories": false,
"recursiveSearch": false,
"viewMode": "single",
"setWallpaperOnAllMonitors": true,
"fillMode": "crop",
"fillColor": "#000000",
+29
View File
@@ -0,0 +1,29 @@
import QtQuick
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration43", "Migrating recursiveSearch to viewMode");
const wallpaper = rawJson?.wallpaper;
if (!wallpaper) {
logger.d("Migration43", "No wallpaper section found, skipping migration");
return true;
}
// Check if already migrated (has viewMode)
if (wallpaper.viewMode !== undefined) {
logger.d("Migration43", "Already has viewMode, skipping migration");
return true;
}
// Migrate recursiveSearch to viewMode
const oldValue = wallpaper.recursiveSearch ?? false;
const newValue = oldValue ? "recursive" : "single";
adapter.wallpaper.viewMode = newValue;
logger.i("Migration43", "Migrated recursiveSearch=" + oldValue + " to viewMode=" + newValue);
return true;
}
}
+3 -1
View File
@@ -16,7 +16,8 @@ QtObject {
37: migration37Component,
38: migration38Component,
40: migration40Component,
42: migration42Component
42: migration42Component,
43: migration43Component
})
// Migration components
@@ -30,4 +31,5 @@ QtObject {
property Component migration38Component: Migration38 {}
property Component migration40Component: Migration40 {}
property Component migration42Component: Migration42 {}
property Component migration43Component: Migration43 {}
}
+2 -2
View File
@@ -25,7 +25,7 @@ Singleton {
- Default cache directory: ~/.cache/noctalia
*/
readonly property alias data: adapter // Used to access via Settings.data.xxx.yyy
readonly property int settingsVersion: 42
readonly property int settingsVersion: 43
readonly property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1"
readonly property string shellName: "noctalia"
readonly property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
@@ -342,7 +342,7 @@ Singleton {
property string directory: ""
property list<var> monitorDirectories: []
property bool enableMultiMonitorDirectories: false
property bool recursiveSearch: false
property string viewMode: "single" // "single" | "recursive" | "browse"
property bool setWallpaperOnAllMonitors: true
property string fillMode: "crop"
property color fillColor: "#000000"
@@ -56,12 +56,27 @@ ColumnLayout {
onButtonClicked: root.openMainFolderPicker()
}
NToggle {
label: I18n.tr("panels.wallpaper.settings-recursive-search-label")
description: I18n.tr("panels.wallpaper.settings-recursive-search-description")
checked: Settings.data.wallpaper.recursiveSearch
onToggled: checked => Settings.data.wallpaper.recursiveSearch = checked
defaultValue: Settings.getDefaultValue("wallpaper.recursiveSearch")
NComboBox {
label: I18n.tr("panels.wallpaper.settings-view-mode-label")
description: I18n.tr("panels.wallpaper.settings-view-mode-description")
Layout.fillWidth: true
model: [
{
"key": "single",
"name": I18n.tr("panels.wallpaper.view-mode-single")
},
{
"key": "recursive",
"name": I18n.tr("panels.wallpaper.view-mode-recursive")
},
{
"key": "browse",
"name": I18n.tr("panels.wallpaper.view-mode-browse")
}
]
currentKey: Settings.data.wallpaper.viewMode
onSelected: key => Settings.data.wallpaper.viewMode = key
defaultValue: Settings.getDefaultValue("wallpaper.viewMode")
}
NToggle {
+231 -68
View File
@@ -106,11 +106,13 @@ SmartPanel {
if (view?.gridView?.activeFocus) {
let gridView = view.gridView;
if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.length) {
let path = gridView.model[gridView.currentIndex];
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(path, undefined);
let item = gridView.model[gridView.currentIndex];
if (item.isDirectory) {
WallpaperService.setBrowsePath(view.targetScreen.name, item.path);
} else if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(item.path, undefined);
} else {
WallpaperService.changeWallpaper(path, view.targetScreen.name);
WallpaperService.changeWallpaper(item.path, view.targetScreen.name);
}
}
}
@@ -302,28 +304,6 @@ SmartPanel {
}
}
NIconButton {
icon: "refresh"
tooltipText: Settings.data.wallpaper.useWallhaven ? I18n.tr("tooltips.refresh-wallhaven") : I18n.tr("tooltips.refresh-wallpaper-list")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (Settings.data.wallpaper.useWallhaven) {
if (typeof WallhavenService !== "undefined") {
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery, 1);
}
} else {
WallpaperService.refreshWallpapersList();
}
}
}
//Hide Wallpaper Filenames
NIconButton {
icon: Settings.data.wallpaper.hideWallpaperFilenames ? "eye-closed" : "eye"
tooltipText: Settings.data.wallpaper.hideWallpaperFilenames ? I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-show") : I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-hide")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.wallpaper.hideWallpaperFilenames = !Settings.data.wallpaper.hideWallpaperFilenames
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
@@ -612,23 +592,52 @@ SmartPanel {
// Local reactive state for this screen
property list<string> wallpapersList: []
property string currentWallpaper: ""
property list<string> filteredWallpapers: []
property var wallpapersWithNames: [] // Cached basenames
property var filteredItems: [] // Combined list of { path, name, isDirectory }
property var wallpapersWithNames: [] // Cached basenames for files
property var directoriesList: [] // List of directories in browse mode
// Browse mode properties
property string currentBrowsePath: WallpaperService.getCurrentBrowsePath(targetScreen?.name ?? "")
property bool isBrowseMode: Settings.data.wallpaper.viewMode === "browse"
// Expose updateFiltered as a proper function property
function updateFiltered() {
var combinedItems = [];
// In browse mode, add directories first
if (isBrowseMode) {
for (var i = 0; i < directoriesList.length; i++) {
var dirPath = directoriesList[i];
combinedItems.push({
"path": dirPath,
"name": dirPath.split('/').pop(),
"isDirectory": true
});
}
}
// Add files
for (var i = 0; i < wallpapersList.length; i++) {
combinedItems.push({
"path": wallpapersList[i],
"name": wallpapersList[i].split('/').pop(),
"isDirectory": false
});
}
// Apply filter if text is present
if (!panelContent.filterText || panelContent.filterText.trim().length === 0) {
filteredWallpapers = wallpapersList;
filteredItems = combinedItems;
return;
}
const results = FuzzySort.go(panelContent.filterText.trim(), wallpapersWithNames, {
const results = FuzzySort.go(panelContent.filterText.trim(), combinedItems, {
"key": 'name',
"limit": 200
});
// Map back to path list
filteredWallpapers = results.map(function (r) {
return r.obj.path;
// Map back to item list
filteredItems = results.map(function (r) {
return r.obj;
});
}
@@ -645,6 +654,10 @@ SmartPanel {
}
function onWallpaperDirectoryChanged(screenName, directory) {
if (targetScreen !== null && screenName === targetScreen.name) {
// Reset browse path when root directory changes
if (isBrowseMode) {
WallpaperService.navigateToRoot(targetScreen.name);
}
refreshWallpaperScreenData();
}
}
@@ -653,31 +666,137 @@ SmartPanel {
refreshWallpaperScreenData();
}
}
function onBrowsePathChanged(screenName, path) {
if (targetScreen !== null && screenName === targetScreen.name) {
currentBrowsePath = path;
refreshWallpaperScreenData();
}
}
}
function refreshWallpaperScreenData() {
if (targetScreen === null) {
return;
}
wallpapersList = WallpaperService.getWallpapersList(targetScreen.name);
Logger.d("WallpaperPanel", "Got", wallpapersList.length, "wallpapers for screen", targetScreen.name);
// Pre-compute basenames once for better performance
wallpapersWithNames = wallpapersList.map(function (p) {
return {
"path": p,
"name": p.split('/').pop()
};
});
currentWallpaper = WallpaperService.getWallpaper(targetScreen.name);
updateFiltered();
if (isBrowseMode) {
// In browse mode, scan current directory for both files and directories
var browsePath = WallpaperService.getCurrentBrowsePath(targetScreen.name);
currentBrowsePath = browsePath;
WallpaperService.scanDirectoryWithDirs(targetScreen.name, browsePath, function(result) {
wallpapersList = result.files;
directoriesList = result.directories;
Logger.d("WallpaperPanel", "Browse mode: Got", wallpapersList.length, "files and", directoriesList.length, "directories for screen", targetScreen.name);
updateFiltered();
});
} else {
// Normal mode: just use the wallpaper list from service
wallpapersList = WallpaperService.getWallpapersList(targetScreen.name);
directoriesList = [];
Logger.d("WallpaperPanel", "Got", wallpapersList.length, "wallpapers for screen", targetScreen.name);
updateFiltered();
}
}
// Helper function to cycle view modes
function cycleViewMode() {
var mode = Settings.data.wallpaper.viewMode;
if (mode === "single") {
Settings.data.wallpaper.viewMode = "recursive";
} else if (mode === "recursive") {
Settings.data.wallpaper.viewMode = "browse";
} else {
Settings.data.wallpaper.viewMode = "single";
}
}
// Helper function to get icon for current view mode
function getViewModeIcon() {
var mode = Settings.data.wallpaper.viewMode;
if (mode === "single") return "folder";
if (mode === "recursive") return "folders";
return "folder-open";
}
// Helper function to get tooltip for current view mode
function getViewModeTooltip() {
var mode = Settings.data.wallpaper.viewMode;
var modeName;
if (mode === "single") modeName = I18n.tr("panels.wallpaper.view-mode-single");
else if (mode === "recursive") modeName = I18n.tr("panels.wallpaper.view-mode-recursive");
else modeName = I18n.tr("panels.wallpaper.view-mode-browse");
return I18n.tr("panels.wallpaper.view-mode-cycle-tooltip").replace("{mode}", modeName);
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM
// Combined toolbar: navigation (left) + actions (right)
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
// Left side: navigation (back, home, path)
NIconButton {
icon: "arrow-left"
tooltipText: I18n.tr("wallpaper.browse.go-up")
enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
onClicked: WallpaperService.navigateUp(targetScreen?.name ?? "")
baseSize: Style.baseWidgetSize * 0.8
}
NIconButton {
icon: "home"
tooltipText: I18n.tr("wallpaper.browse.go-root")
enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
onClicked: WallpaperService.navigateToRoot(targetScreen?.name ?? "")
baseSize: Style.baseWidgetSize * 0.8
}
NScrollText {
text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
Layout.fillWidth: true
scrollMode: NScrollText.ScrollMode.Hover
NText {
text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
// Right side: actions (view mode, hide filenames, refresh)
NIconButton {
icon: getViewModeIcon()
tooltipText: getViewModeTooltip()
baseSize: Style.baseWidgetSize * 0.8
onClicked: cycleViewMode()
}
NIconButton {
icon: Settings.data.wallpaper.hideWallpaperFilenames ? "eye-closed" : "eye"
tooltipText: Settings.data.wallpaper.hideWallpaperFilenames ? I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-show") : I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-hide")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.wallpaper.hideWallpaperFilenames = !Settings.data.wallpaper.hideWallpaperFilenames
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("tooltips.refresh-wallpaper-list")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (isBrowseMode) {
refreshWallpaperScreenData();
} else {
WallpaperService.refreshWallpapersList();
}
}
}
}
GridView {
id: wallpaperGridView
@@ -692,7 +811,7 @@ SmartPanel {
keyNavigationWraps: false
currentIndex: -1
model: filteredWallpapers
model: filteredItems
onModelChanged: {
// Reset selection when model changes
@@ -738,12 +857,14 @@ SmartPanel {
Keys.onPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < filteredWallpapers.length) {
let path = filteredWallpapers[currentIndex];
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(path, undefined);
if (currentIndex >= 0 && currentIndex < filteredItems.length) {
let item = filteredItems[currentIndex];
if (item.isDirectory) {
WallpaperService.setBrowsePath(targetScreen.name, item.path);
} else if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(item.path, undefined);
} else {
WallpaperService.changeWallpaper(path, targetScreen.name);
WallpaperService.changeWallpaper(item.path, targetScreen.name);
}
}
event.accepted = true;
@@ -808,20 +929,21 @@ SmartPanel {
anchors.fill: parent
anchors.margins: Style.marginXS
property string wallpaperPath: modelData
property bool isSelected: (wallpaperPath === currentWallpaper)
property string filename: wallpaperPath.split('/').pop()
property string wallpaperPath: modelData.path ?? ""
property bool isDirectory: modelData.isDirectory ?? false
property bool isSelected: !isDirectory && (wallpaperPath === currentWallpaper)
property string filename: modelData.name ?? wallpaperPath.split('/').pop()
property string cachedPath: ""
spacing: Style.marginXS
Component.onCompleted: {
if (ImageCacheService.initialized) {
if (!isDirectory && ImageCacheService.initialized) {
ImageCacheService.getThumbnail(wallpaperPath, function (path, success) {
if (wallpaperItem)
wallpaperItem.cachedPath = success ? path : wallpaperPath;
});
} else {
} else if (!isDirectory) {
cachedPath = wallpaperPath;
}
}
@@ -829,11 +951,43 @@ SmartPanel {
Item {
id: imageContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(wallpaperGridView.itemSize * 0.67)
Layout.fillHeight: true
property real imageHeight: Math.round(wallpaperGridView.itemSize * 0.67)
// Directory display
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: imageContainer.imageHeight
color: Color.mSurfaceVariant
radius: Style.radiusM
visible: wallpaperItem.isDirectory
border.color: wallpaperGridView.currentIndex === index ? Color.mHover : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5)
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "folder"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
}
}
// Image display (for non-directories)
NImageRounded {
id: img
anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: imageContainer.imageHeight
visible: !wallpaperItem.isDirectory
imagePath: wallpaperItem.cachedPath
radius: Style.radiusM
borderColor: {
@@ -849,12 +1003,15 @@ SmartPanel {
imageFillMode: Image.PreserveAspectCrop
}
// Loading/error state background
// Loading/error state background (for non-directories)
Rectangle {
anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: imageContainer.imageHeight
color: Color.mSurfaceVariant
radius: Style.radiusM
visible: img.status === Image.Loading || img.status === Image.Error || wallpaperItem.cachedPath === ""
visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || img.status === Image.Error || wallpaperItem.cachedPath === "")
NIcon {
icon: "image"
@@ -865,8 +1022,9 @@ SmartPanel {
}
NBusyIndicator {
anchors.centerIn: parent
visible: img.status === Image.Loading || wallpaperItem.cachedPath === ""
anchors.horizontalCenter: parent.horizontalCenter
y: (imageContainer.imageHeight - height) / 2
visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || wallpaperItem.cachedPath === "")
running: visible
size: 18
}
@@ -892,7 +1050,10 @@ SmartPanel {
}
Rectangle {
anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: imageContainer.imageHeight
color: Color.mSurface
radius: Style.radiusM
opacity: (hoverHandler.hovered || wallpaperItem.isSelected || wallpaperGridView.currentIndex === index) ? 0 : 0.3
@@ -911,7 +1072,9 @@ SmartPanel {
onTapped: {
wallpaperGridView.forceActiveFocus();
wallpaperGridView.currentIndex = index;
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
if (wallpaperItem.isDirectory) {
WallpaperService.setBrowsePath(targetScreen.name, wallpaperItem.wallpaperPath);
} else if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(wallpaperItem.wallpaperPath, undefined);
} else {
WallpaperService.changeWallpaper(wallpaperItem.wallpaperPath, targetScreen.name);
@@ -942,7 +1105,7 @@ SmartPanel {
radius: Style.radiusM
border.color: Color.mOutline
border.width: Style.borderS
visible: (filteredWallpapers.length === 0 && !WallpaperService.scanning) || WallpaperService.scanning
visible: (filteredItems.length === 0 && !WallpaperService.scanning) || WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: 130
@@ -956,7 +1119,7 @@ SmartPanel {
ColumnLayout {
anchors.fill: parent
visible: filteredWallpapers.length === 0 && !WallpaperService.scanning
visible: filteredItems.length === 0 && !WallpaperService.scanning
Item {
Layout.fillHeight: true
}
@@ -967,13 +1130,13 @@ SmartPanel {
Layout.alignment: Qt.AlignHCenter
}
NText {
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.no-match") : I18n.tr("wallpaper.no-wallpaper")
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.no-match") : (isBrowseMode ? I18n.tr("wallpaper.browse.empty-directory") : I18n.tr("wallpaper.no-wallpaper"))
color: Color.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.try-different-search") : I18n.tr("wallpaper.configure-directory")
text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.try-different-search") : (isBrowseMode ? I18n.tr("wallpaper.browse.go-up-hint") : I18n.tr("wallpaper.configure-directory"))
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
+203 -122
View File
@@ -1,5 +1,4 @@
pragma Singleton
import Qt.labs.folderlistmodel
import QtQuick
import Quickshell
@@ -48,6 +47,13 @@ Singleton {
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: ({})
// Signal emitted when browse path changes for a screen
signal browsePathChanged(string screenName, string path)
Connections {
target: Settings.data.wallpaper
function onDirectoryChanged() {
@@ -91,7 +97,9 @@ Singleton {
root.setNextWallpaper();
}
}
function onRecursiveSearchChanged() {
function onViewModeChanged() {
// Reset browse paths to root when mode changes
root.currentBrowsePaths = {};
root.refreshWallpapersList();
}
function onUseSolidColorChanged() {
@@ -475,48 +483,160 @@ Singleton {
return [];
}
// -------------------------------------------------------------------
// Browse mode helper functions
// -------------------------------------------------------------------
function getCurrentBrowsePath(screenName) {
if (currentBrowsePaths[screenName] !== undefined) {
return 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 go above the root directory
if (currentPath === rootPath) return;
// Get parent directory
var parentPath = currentPath.replace(/\/[^\/]+\/?$/, "");
if (parentPath === "") parentPath = "/";
// 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) {
// Sort both lists
result.files.sort();
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 !== '') {
dirs.push(line);
}
}
}
callback(dirs);
processObject.destroy();
});
processObject.running = true;
}
// -------------------------------------------------------------------
function refreshWallpapersList() {
Logger.d("Wallpaper", "refreshWallpapersList", "recursive:", Settings.data.wallpaper.recursiveSearch);
var mode = Settings.data.wallpaper.viewMode;
Logger.d("Wallpaper", "refreshWallpapersList", "viewMode:", mode);
scanningCount = 0;
if (Settings.data.wallpaper.recursiveSearch) {
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 {
// Use FolderListModel (non-recursive)
// Force refresh by toggling each scanner's currentDirectory
for (var i = 0; i < wallpaperScanners.count; i++) {
var scanner = wallpaperScanners.objectAt(i);
if (scanner) {
// Capture scanner in closure
(function (s) {
var directory = root.getMonitorDirectory(s.screenName);
// Trigger a change by setting to /tmp (always exists) then back to the actual directory
// Note: This causes harmless Qt warnings (QTBUG-52262) but is necessary to force FolderListModel to re-scan
s.currentDirectory = "/tmp";
Qt.callLater(function () {
s.currentDirectory = directory;
});
})(scanner);
}
// 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);
}
}
}
// Process instances for recursive scanning (one per screen)
property var recursiveProcesses: ({})
// -------------------------------------------------------------------
function scanDirectoryRecursive(screenName, directory) {
// 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);
wallpaperLists[screenName] = [];
wallpaperListChanged(screenName, 0);
if (updateList) {
wallpaperLists[screenName] = [];
wallpaperListChanged(screenName, 0);
}
if (callback) callback([]);
return;
}
@@ -526,15 +646,22 @@ Singleton {
recursiveProcesses[screenName].running = false;
recursiveProcesses[screenName].destroy();
delete recursiveProcesses[screenName];
scanningCount--;
if (updateList) scanningCount--;
}
scanningCount++;
Logger.i("Wallpaper", "Starting recursive scan for", screenName, "in", directory);
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, "-type", "f", "("];
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");
@@ -545,29 +672,31 @@ Singleton {
findArgs.push(")");
// Create Process component inline
var processComponent = Qt.createComponent("", root);
var processString = `
import QtQuick
import Quickshell.Io
Process {
id: process
command: ` + JSON.stringify(findArgs) + `
stdout: StdioCollector {}
stderr: StdioCollector {}
id: process
command: ${JSON.stringify(findArgs)}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
`;
var processObject = Qt.createQmlObject(processString, root, "RecursiveScan_" + screenName);
var processObject = Qt.createQmlObject(processString, root, "Scan_" + screenName);
// Store reference to avoid garbage collection
recursiveProcesses[screenName] = processObject;
if (updateList) {
recursiveProcesses[screenName] = processObject;
}
var handler = function (exitCode) {
scanningCount--;
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 files = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
@@ -576,29 +705,37 @@ Singleton {
}
// Sort files for consistent ordering
files.sort();
wallpaperLists[screenName] = files;
// Reset alphabetical indices when list changes
if (alphabeticalIndices[screenName] !== undefined) {
// Reset to 0 or find current wallpaper in new list
var currentWallpaper = currentWallpapers[screenName] || "";
var foundIndex = files.indexOf(currentWallpaper);
alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0;
if (updateList) {
wallpaperLists[screenName] = files;
// Reset alphabetical indices when list changes
if (alphabeticalIndices[screenName] !== undefined) {
var currentWallpaper = currentWallpapers[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);
}
Logger.i("Wallpaper", "Recursive scan completed for", screenName, "found", files.length, "files");
wallpaperListChanged(screenName, files.length);
} else {
Logger.w("Wallpaper", "Recursive scan failed for", screenName, "exit code:", exitCode, "(directory might not exist)");
wallpaperLists[screenName] = [];
// Reset alphabetical index when list is empty
if (alphabeticalIndices[screenName] !== undefined) {
alphabeticalIndices[screenName] = 0;
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);
}
wallpaperListChanged(screenName, 0);
}
// Clean up
delete recursiveProcesses[screenName];
if (updateList) {
delete recursiveProcesses[screenName];
}
if (callback) callback(files);
processObject.destroy();
};
@@ -607,6 +744,14 @@ Singleton {
processObject.running = true;
}
// Process instances for scanning (one per screen)
property var recursiveProcesses: ({})
// -------------------------------------------------------------------
function scanDirectoryRecursive(screenName, directory) {
_scanDirectoryInternal(screenName, directory, true, true, null);
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// -------------------------------------------------------------------
@@ -619,70 +764,6 @@ Singleton {
triggeredOnStart: false
}
// Instantiator (not Repeater) to create FolderListModel for each monitor
Instantiator {
id: wallpaperScanners
model: Quickshell.screens
delegate: FolderListModel {
property string screenName: modelData.name
property string currentDirectory: root.getMonitorDirectory(screenName)
folder: "file://" + currentDirectory
nameFilters: ImageCacheService.imageFilters
caseSensitive: false
showDirs: false
sortField: FolderListModel.Name
// Watch for directory changes via property binding
onCurrentDirectoryChanged: {
folder = "file://" + currentDirectory;
}
Component.onCompleted: {
// Connect to directory change signal
root.wallpaperDirectoryChanged.connect(function (screen, directory) {
if (screen === screenName) {
currentDirectory = directory;
}
});
}
onStatusChanged: {
if (status === FolderListModel.Null) {
// Flush the list
root.wallpaperLists[screenName] = [];
root.wallpaperListChanged(screenName, 0);
} else if (status === FolderListModel.Loading) {
// Flush the list
root.wallpaperLists[screenName] = [];
scanningCount++;
} else if (status === FolderListModel.Ready) {
var files = [];
for (var i = 0; i < count; i++) {
var directory = root.getMonitorDirectory(screenName);
var filepath = directory + "/" + get(i, "fileName");
files.push(filepath);
}
// Update the list
root.wallpaperLists[screenName] = files;
// Reset alphabetical indices when list changes
if (root.alphabeticalIndices[screenName] !== undefined) {
// Reset to 0 or find current wallpaper in new list
var currentWallpaper = root.currentWallpapers[screenName] || "";
var foundIndex = files.indexOf(currentWallpaper);
root.alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0;
}
scanningCount--;
Logger.d("Wallpaper", "List refreshed for", screenName, "count:", files.length);
root.wallpaperListChanged(screenName, files.length);
}
}
}
}
// -------------------------------------------------------------------
// Cache file persistence
// -------------------------------------------------------------------