mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
wallpaper: ability to manually browse into subfolders, service cleanup, ui improvements
This commit is contained in:
@@ -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)",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"directory": "",
|
||||
"monitorDirectories": [],
|
||||
"enableMultiMonitorDirectories": false,
|
||||
"recursiveSearch": false,
|
||||
"viewMode": "single",
|
||||
"setWallpaperOnAllMonitors": true,
|
||||
"fillMode": "crop",
|
||||
"fillColor": "#000000",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user