diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index b9bfabe89..a4be8e713 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -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)", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index b1282ef6b..7344fcdc8 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -139,7 +139,7 @@ "directory": "", "monitorDirectories": [], "enableMultiMonitorDirectories": false, - "recursiveSearch": false, + "viewMode": "single", "setWallpaperOnAllMonitors": true, "fillMode": "crop", "fillColor": "#000000", diff --git a/Commons/Migrations/Migration43.qml b/Commons/Migrations/Migration43.qml new file mode 100644 index 000000000..93daf31eb --- /dev/null +++ b/Commons/Migrations/Migration43.qml @@ -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; + } +} diff --git a/Commons/Migrations/MigrationRegistry.qml b/Commons/Migrations/MigrationRegistry.qml index 347dddf61..776243073 100644 --- a/Commons/Migrations/MigrationRegistry.qml +++ b/Commons/Migrations/MigrationRegistry.qml @@ -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 {} } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 399295123..8d76a57be 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -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 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" diff --git a/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml b/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml index cff1ec056..7a1ef5253 100644 --- a/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml @@ -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 { diff --git a/Modules/Panels/Wallpaper/WallpaperPanel.qml b/Modules/Panels/Wallpaper/WallpaperPanel.qml index bf4cbaa87..727219c51 100644 --- a/Modules/Panels/Wallpaper/WallpaperPanel.qml +++ b/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -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 wallpapersList: [] property string currentWallpaper: "" - property list 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 diff --git a/Services/UI/WallpaperService.qml b/Services/UI/WallpaperService.qml index b89d35ae9..d0cb58fd9 100644 --- a/Services/UI/WallpaperService.qml +++ b/Services/UI/WallpaperService.qml @@ -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 // -------------------------------------------------------------------