forked from Shinonome/dots-hyprland
wallpaper selector: show folders
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,4 +149,4 @@ Item {
|
|||||||
}
|
}
|
||||||
onClicked: root.activated()
|
onClicked: root.activated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
@@ -102,7 +100,7 @@ Item {
|
|||||||
id: quickDirColumnLayout
|
id: quickDirColumnLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.margins: 12
|
Layout.margins: 12
|
||||||
font {
|
font {
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user