From 8bbf040100a02c927a556b486267b6906810d6b0 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 12:20:04 +0700 Subject: [PATCH] wallpaper selector: show folders --- .../ii/modules/common/Directories.qml | 1 + .../quickshell/ii/modules/common/Images.qml | 12 +++ .../common/{ => widgets}/AddressBar.qml | 0 .../modules/common/widgets/DirectoryIcon.qml | 42 ++++++++++ .../WallpaperDirectoryItem.qml | 41 ++++++--- .../WallpaperSelectorContent.qml | 84 +++++++++++++------ .config/quickshell/ii/services/Wallpapers.qml | 37 ++++++-- 7 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/Images.qml rename .config/quickshell/ii/modules/common/{ => widgets}/AddressBar.qml (100%) create mode 100644 .config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index 49f0e46da..60f587b59 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -16,6 +16,7 @@ Singleton { readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0] readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0] // Other dirs used by the shell, without "file://" diff --git a/.config/quickshell/ii/modules/common/Images.qml b/.config/quickshell/ii/modules/common/Images.qml new file mode 100644 index 000000000..ac76f5118 --- /dev/null +++ b/.config/quickshell/ii/modules/common/Images.qml @@ -0,0 +1,12 @@ +pragma Singleton + +import Quickshell + +Singleton { + readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] + readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + + function isValidImageByName(name: string): bool { + return validImageExtensions.some(t => name.endsWith(`.${t}`)); + } +} diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml similarity index 100% rename from .config/quickshell/ii/modules/common/AddressBar.qml rename to .config/quickshell/ii/modules/common/widgets/AddressBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml new file mode 100644 index 000000000..b31ad021c --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -0,0 +1,42 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common + +// From https://github.com/caelestia-dots/shell with modifications. +// License: GPLv3 + +Image { + id: root + required property var fileModelData + asynchronous: true + fillMode: Image.PreserveAspectFit + + source: { + if (!fileModelData.fileIsDir) + return Quickshell.iconPath("application-x-zerosize"); + + const name = fileModelData.fileName; + const homeDir = Directories.home + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(name)) + return Quickshell.iconPath(`folder-${name.toLowerCase()}`); + + return Quickshell.iconPath("inode-directory"); + } + + onStatusChanged: { + if (status === Image.Error) + source = Quickshell.iconPath("error"); + } + + Process { + running: !fileModelData.fileIsDir + command: ["file", "--mime", "-b", fileModelData.filePath] + stdout: StdioCollector { + onStreamFinished: { + const mime = text.split(";")[0].replace("/", "-"); + root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing"); + } + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 1e940ece2..c1f0b83c9 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -12,14 +12,16 @@ import Quickshell.Io Item { id: root - required property string path + required property var fileModelData + property bool isDirectory: fileModelData.fileIsDir + property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) property bool isHovered: false property alias color: background.color property alias radius: background.radius property alias padding: background.anchors.margins - signal activated() + signal activated Rectangle { id: background @@ -45,21 +47,27 @@ Item { Layout.fillHeight: true Layout.fillWidth: true - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small + Loader { + id: thumbnailShadowLoader + active: thumbnailImageLoader.active && thumbnailImageLoader.item.status === Image.Ready + anchors.fill: thumbnailImageLoader + sourceComponent: StyledRectangularShadow { + target: thumbnailImageLoader + anchors.fill: undefined + radius: Appearance.rounding.small + } } Loader { id: thumbnailImageLoader anchors.fill: parent - active: root.visible + active: root.useThumbnail sourceComponent: Image { id: thumbnailImage source: { - if (root.path.length == 0) + if (fileModelData.filePath.length == 0) return; - const resolvedUrl = Qt.resolvedUrl(root.path); + const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath); const md5Hash = Qt.md5(resolvedUrl); const cacheSize = "normal"; const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; @@ -79,6 +87,8 @@ Item { Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } + onStatusChanged: if (status === Image.Error) + root.useThumbnail = false layer.enabled: true layer.effect: OpacityMask { @@ -90,6 +100,17 @@ Item { } } } + + Loader { + id: iconLoader + active: !root.useThumbnail + anchors.fill: parent + sourceComponent: DirectoryIcon { + fileModelData: root.fileModelData + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + } + } } StyledText { @@ -105,7 +126,7 @@ Item { Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } - text: FileUtils.fileNameForPath(root.path) + text: fileModelData.fileName } } } @@ -128,4 +149,4 @@ Item { } onClicked: root.activated() } -} \ No newline at end of file +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 2fc14a9ef..f6c19b5c1 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -14,8 +14,6 @@ Item { id: root property int columns: 4 property real previewCellAspectRatio: 4 / 3 - property var wallpapers: Wallpapers.wallpapers - property string filterQuery: "" property bool useDarkMode: Appearance.m3colors.darkmode Keys.onPressed: event => { @@ -102,7 +100,7 @@ Item { id: quickDirColumnLayout anchors.fill: parent spacing: 0 - + StyledText { Layout.margins: 12 font { @@ -118,14 +116,46 @@ Item { implicitWidth: 140 clip: true model: [ - { icon: "home", name: "Home", path: Directories.home }, - { icon: "folder", name: "Documents", path: Directories.documents }, - { icon: "download", name: "Downloads", path: Directories.downloads }, - { icon: "image", name: "Pictures", path: Directories.pictures }, - { icon: "movie", name: "Videos", path: Directories.videos }, - { icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" }, - { icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` }, - { icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` }, + { + icon: "home", + name: "Home", + path: Directories.home + }, + { + icon: "folder", + name: "Documents", + path: Directories.documents + }, + { + icon: "download", + name: "Downloads", + path: Directories.downloads + }, + { + icon: "image", + name: "Pictures", + path: Directories.pictures + }, + { + icon: "movie", + name: "Videos", + path: Directories.videos + }, + { + icon: "", + name: "---", + path: "INTENTIONALLY_INVALID_DIR" + }, + { + icon: "wallpaper", + name: "Wallpapers", + path: `${Directories.pictures}/Wallpapers` + }, + { + icon: "favorite", + name: "Homework", + path: `${Directories.pictures}/homework` + }, ] delegate: RippleButton { id: quickDirButton @@ -191,7 +221,7 @@ Item { GridView { id: grid - visible: root.wallpapers.length > 0 + visible: Wallpapers.folderModel.count > 0 readonly property int columns: root.columns readonly property int rows: Math.max(1, Math.ceil(count / columns)) @@ -217,36 +247,29 @@ Item { item.isHovered = false; } } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); } function activateCurrent() { - print("ACTIVATE"); - const path = grid.model.values[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; + const filePath = grid.model.get(currentIndex, "filePath") + Wallpapers.select(filePath, root.useDarkMode); filterField.text = ""; - Wallpapers.apply(path, root.useDarkMode); } - model: ScriptModel { - values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) - } + model: Wallpapers.folderModel onModelChanged: currentIndex = 0 delegate: WallpaperDirectoryItem { required property var modelData required property int index - visible: modelData.length > 0 + fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - path: modelData color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) onActivated: { - Wallpapers.apply(path, root.useDarkMode); - GlobalStates.wallpaperSelectorOpen = false; + Wallpapers.select(fileModelData.filePath, root.useDarkMode); + filterField.text = ""; } } @@ -339,7 +362,7 @@ Item { } onTextChanged: { - root.filterQuery = text; + Wallpapers.searchQuery = text; } Keys.onPressed: event => { @@ -389,4 +412,11 @@ Item { } } } + + Connections { + target: Wallpapers + function onChanged() { + GlobalStates.wallpaperSelectorOpen = false; + } + } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d01e38034..b98d3fe70 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,12 +15,15 @@ Singleton { id: root property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + property alias folderModel: folderModel // Expose for direct binding when needed + property string searchQuery: "" readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] - property alias filesModel: files // Expose for direct binding when needed property list wallpapers: [] // List of absolute file paths (without file://) + signal changed() + // Executions Process { id: applyProc @@ -37,6 +40,29 @@ Singleton { "--image", path, "--mode", (darkMode ? "dark" : "light") ]) + root.changed() + } + + Process { + id: selectProc + property string filePath: "" + property bool darkMode: Appearance.m3colors.darkmode + function select(filePath, darkMode = Appearance.m3colors.darkmode) { + selectProc.filePath = filePath + selectProc.darkMode = darkMode + selectProc.exec(["test", "-d", FileUtils.trimFileProtocol(filePath)]) + } + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + setDirectory(selectProc.filePath); + return; + } + root.apply(selectProc.filePath, selectProc.darkMode); + } + } + + function select(filePath, darkMode = Appearance.m3colors.darkmode) { + selectProc.select(filePath); } Process { @@ -59,9 +85,10 @@ Singleton { // Folder model FolderListModel { - id: files + id: folderModel folder: Qt.resolvedUrl(root.directory) - nameFilters: root.extensions.map(ext => `*.${ext}`) + caseSensitive: false + nameFilters: root.extensions.map(ext => `*${searchQuery.split(" ").filter(s => s.length > 0).map(s => `*${s}*`)}*.${ext}`) showDirs: true showDotAndDotDot: false showOnlyReadable: true @@ -69,8 +96,8 @@ Singleton { sortReversed: false onCountChanged: { root.wallpapers = [] - for (let i = 0; i < files.count; i++) { - const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) + for (let i = 0; i < folderModel.count; i++) { + const path = folderModel.get(i, "filePath") || FileUtils.trimFileProtocol(folderModel.get(i, "fileURL")) if (path && path.length) root.wallpapers.push(path) } }