wallpaper selector: show folders

This commit is contained in:
end-4
2025-08-24 12:20:04 +07:00
parent bdc0ade117
commit 8bbf040100
7 changed files with 175 additions and 42 deletions
@@ -16,6 +16,7 @@ Singleton {
readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0] readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[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] readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
// Other dirs used by the shell, without "file://" // Other dirs used by the shell, without "file://"
@@ -0,0 +1,12 @@
pragma Singleton
import Quickshell
Singleton {
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"]
function isValidImageByName(name: string): bool {
return validImageExtensions.some(t => name.endsWith(`.${t}`));
}
}
@@ -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");
}
}
}
}
@@ -12,14 +12,16 @@ import Quickshell.Io
Item { Item {
id: root 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 bool isHovered: false
property alias color: background.color property alias color: background.color
property alias radius: background.radius property alias radius: background.radius
property alias padding: background.anchors.margins property alias padding: background.anchors.margins
signal activated() signal activated
Rectangle { Rectangle {
id: background id: background
@@ -45,21 +47,27 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
StyledRectangularShadow { Loader {
target: thumbnailImageLoader id: thumbnailShadowLoader
radius: Appearance.rounding.small active: thumbnailImageLoader.active && thumbnailImageLoader.item.status === Image.Ready
anchors.fill: thumbnailImageLoader
sourceComponent: StyledRectangularShadow {
target: thumbnailImageLoader
anchors.fill: undefined
radius: Appearance.rounding.small
}
} }
Loader { Loader {
id: thumbnailImageLoader id: thumbnailImageLoader
anchors.fill: parent anchors.fill: parent
active: root.visible active: root.useThumbnail
sourceComponent: Image { sourceComponent: Image {
id: thumbnailImage id: thumbnailImage
source: { source: {
if (root.path.length == 0) if (fileModelData.filePath.length == 0)
return; return;
const resolvedUrl = Qt.resolvedUrl(root.path); const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath);
const md5Hash = Qt.md5(resolvedUrl); const md5Hash = Qt.md5(resolvedUrl);
const cacheSize = "normal"; const cacheSize = "normal";
const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`;
@@ -79,6 +87,8 @@ Item {
Behavior on opacity { Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
onStatusChanged: if (status === Image.Error)
root.useThumbnail = false
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { 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 { StyledText {
@@ -105,7 +126,7 @@ Item {
Behavior on color { Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
} }
text: FileUtils.fileNameForPath(root.path) text: fileModelData.fileName
} }
} }
} }
@@ -14,8 +14,6 @@ Item {
id: root id: root
property int columns: 4 property int columns: 4
property real previewCellAspectRatio: 4 / 3 property real previewCellAspectRatio: 4 / 3
property var wallpapers: Wallpapers.wallpapers
property string filterQuery: ""
property bool useDarkMode: Appearance.m3colors.darkmode property bool useDarkMode: Appearance.m3colors.darkmode
Keys.onPressed: event => { Keys.onPressed: event => {
@@ -118,14 +116,46 @@ Item {
implicitWidth: 140 implicitWidth: 140
clip: true clip: true
model: [ model: [
{ icon: "home", name: "Home", path: Directories.home }, {
{ icon: "folder", name: "Documents", path: Directories.documents }, icon: "home",
{ icon: "download", name: "Downloads", path: Directories.downloads }, name: "Home",
{ icon: "image", name: "Pictures", path: Directories.pictures }, path: Directories.home
{ icon: "movie", name: "Videos", path: Directories.videos }, },
{ icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" }, {
{ icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` }, icon: "folder",
{ icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` }, 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 { delegate: RippleButton {
id: quickDirButton id: quickDirButton
@@ -191,7 +221,7 @@ Item {
GridView { GridView {
id: grid id: grid
visible: root.wallpapers.length > 0 visible: Wallpapers.folderModel.count > 0
readonly property int columns: root.columns readonly property int columns: root.columns
readonly property int rows: Math.max(1, Math.ceil(count / columns)) readonly property int rows: Math.max(1, Math.ceil(count / columns))
@@ -217,36 +247,29 @@ Item {
item.isHovered = false; 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); positionViewAtIndex(currentIndex, GridView.Contain);
} }
function activateCurrent() { function activateCurrent() {
print("ACTIVATE"); const filePath = grid.model.get(currentIndex, "filePath")
const path = grid.model.values[currentIndex]; Wallpapers.select(filePath, root.useDarkMode);
if (!path)
return;
GlobalStates.wallpaperSelectorOpen = false;
filterField.text = ""; filterField.text = "";
Wallpapers.apply(path, root.useDarkMode);
} }
model: ScriptModel { model: Wallpapers.folderModel
values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase())))
}
onModelChanged: currentIndex = 0 onModelChanged: currentIndex = 0
delegate: WallpaperDirectoryItem { delegate: WallpaperDirectoryItem {
required property var modelData required property var modelData
required property int index required property int index
visible: modelData.length > 0 fileModelData: modelData
width: grid.cellWidth width: grid.cellWidth
height: grid.cellHeight height: grid.cellHeight
path: modelData
color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary)
onActivated: { onActivated: {
Wallpapers.apply(path, root.useDarkMode); Wallpapers.select(fileModelData.filePath, root.useDarkMode);
GlobalStates.wallpaperSelectorOpen = false;
filterField.text = ""; filterField.text = "";
} }
} }
@@ -339,7 +362,7 @@ Item {
} }
onTextChanged: { onTextChanged: {
root.filterQuery = text; Wallpapers.searchQuery = text;
} }
Keys.onPressed: event => { Keys.onPressed: event => {
@@ -389,4 +412,11 @@ Item {
} }
} }
} }
Connections {
target: Wallpapers
function onChanged() {
GlobalStates.wallpaperSelectorOpen = false;
}
}
} }
+32 -5
View File
@@ -15,12 +15,15 @@ Singleton {
id: root id: root
property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`)
property alias folderModel: folderModel // Expose for direct binding when needed
property string searchQuery: ""
readonly property list<string> extensions: [ // TODO: add videos readonly property list<string> extensions: [ // TODO: add videos
"jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg"
] ]
property alias filesModel: files // Expose for direct binding when needed
property list<string> wallpapers: [] // List of absolute file paths (without file://) property list<string> wallpapers: [] // List of absolute file paths (without file://)
signal changed()
// Executions // Executions
Process { Process {
id: applyProc id: applyProc
@@ -37,6 +40,29 @@ Singleton {
"--image", path, "--image", path,
"--mode", (darkMode ? "dark" : "light") "--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 { Process {
@@ -59,9 +85,10 @@ Singleton {
// Folder model // Folder model
FolderListModel { FolderListModel {
id: files id: folderModel
folder: Qt.resolvedUrl(root.directory) 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 showDirs: true
showDotAndDotDot: false showDotAndDotDot: false
showOnlyReadable: true showOnlyReadable: true
@@ -69,8 +96,8 @@ Singleton {
sortReversed: false sortReversed: false
onCountChanged: { onCountChanged: {
root.wallpapers = [] root.wallpapers = []
for (let i = 0; i < files.count; i++) { for (let i = 0; i < folderModel.count; i++) {
const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) const path = folderModel.get(i, "filePath") || FileUtils.trimFileProtocol(folderModel.get(i, "fileURL"))
if (path && path.length) root.wallpapers.push(path) if (path && path.length) root.wallpapers.push(path)
} }
} }