forked from Shinonome/dots-hyprland
feat: add wallpaper selector menu (#1820)
This commit is contained in:
@@ -48,7 +48,8 @@ bindld = Super+Shift,M, Toggle mute, exec, wpctl set-mute @DEFAULT_SINK@ toggle
|
|||||||
bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
||||||
bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
||||||
bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
|
||||||
bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # Change wallpaper
|
bindd = Ctrl+Super, T, Toggle wallpaper selector, global, quickshell:wallpaperSelectorToggle # Wallpaper selector
|
||||||
|
bindd = Ctrl+Super, T, Change wallpaper, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # [hidden] Change wallpaper (fallback)
|
||||||
bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets
|
bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets
|
||||||
|
|
||||||
##! Utilities
|
##! Utilities
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ layerrule = animation slide, quickshell:verticalBar
|
|||||||
layerrule = animation fade, quickshell:screenCorners
|
layerrule = animation fade, quickshell:screenCorners
|
||||||
layerrule = animation slide right, quickshell:sidebarRight
|
layerrule = animation slide right, quickshell:sidebarRight
|
||||||
layerrule = animation slide left, quickshell:sidebarLeft
|
layerrule = animation slide left, quickshell:sidebarLeft
|
||||||
|
layerrule = animation slide top, quickshell:wallpaperSelector
|
||||||
layerrule = animation slide bottom, quickshell:osk
|
layerrule = animation slide bottom, quickshell:osk
|
||||||
layerrule = animation slide bottom, quickshell:dock
|
layerrule = animation slide bottom, quickshell:dock
|
||||||
layerrule = blur, quickshell:session
|
layerrule = blur, quickshell:session
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Singleton {
|
|||||||
property bool osdVolumeOpen: false
|
property bool osdVolumeOpen: false
|
||||||
property bool oskOpen: false
|
property bool oskOpen: false
|
||||||
property bool overviewOpen: false
|
property bool overviewOpen: false
|
||||||
|
property bool wallpaperSelectorOpen: false
|
||||||
property bool screenLocked: false
|
property bool screenLocked: false
|
||||||
property bool screenLockContainsCharacters: false
|
property bool screenLockContainsCharacters: false
|
||||||
property bool screenUnlockFailed: false
|
property bool screenUnlockFailed: false
|
||||||
|
|||||||
@@ -350,6 +350,10 @@ Singleton {
|
|||||||
property real baseVerticalBarWidth: 46
|
property real baseVerticalBarWidth: 46
|
||||||
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
|
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
|
||||||
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
|
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
|
||||||
|
property real wallpaperSelectorWidth: 1200
|
||||||
|
property real wallpaperSelectorHeight: 690
|
||||||
|
property real wallpaperSelectorItemMargins: 8
|
||||||
|
property real wallpaperSelectorItemPadding: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
|
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import Quickshell
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
// XDG Dirs, with "file://"
|
// XDG Dirs, with "file://"
|
||||||
|
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
|
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
|
||||||
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
|
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
|
||||||
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
|
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
|
||||||
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[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 music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[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://"
|
||||||
property string assetsPath: Quickshell.shellPath("assets")
|
property string assetsPath: Quickshell.shellPath("assets")
|
||||||
property string scriptPath: Quickshell.shellPath("scripts")
|
property string scriptPath: Quickshell.shellPath("scripts")
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
// Formats
|
||||||
|
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}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnails
|
||||||
|
// https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html
|
||||||
|
readonly property var thumbnailSizes: ({
|
||||||
|
"normal": 128,
|
||||||
|
"large": 256,
|
||||||
|
"x-large": 512,
|
||||||
|
"xx-large": 1024
|
||||||
|
})
|
||||||
|
function thumbnailSizeNameForDimensions(width: int, height: int): string {
|
||||||
|
const sizeNames = Object.keys(thumbnailSizes);
|
||||||
|
for(let i = 0; i < sizeNames.length; i++) {
|
||||||
|
const sizeName = sizeNames[i];
|
||||||
|
const maxSize = thumbnailSizes[sizeName];
|
||||||
|
if (width <= maxSize && height <= maxSize) return sizeName;
|
||||||
|
}
|
||||||
|
return "xx-large";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail image. It currently generates to the right place at the right size, but does not handle metadata/maintenance on modification.
|
||||||
|
* See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
|
||||||
|
*/
|
||||||
|
Image {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool generateThumbnail: true
|
||||||
|
required property string sourcePath
|
||||||
|
property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height)
|
||||||
|
property string thumbnailPath: {
|
||||||
|
if (sourcePath.length == 0) return;
|
||||||
|
const resolvedUrl = Qt.resolvedUrl(sourcePath);
|
||||||
|
const md5Hash = Qt.md5(resolvedUrl);
|
||||||
|
return `${Directories.genericCache}/thumbnails/${thumbnailSizeName}/${md5Hash}.png`;
|
||||||
|
}
|
||||||
|
source: thumbnailPath
|
||||||
|
|
||||||
|
asynchronous: true
|
||||||
|
cache: false
|
||||||
|
smooth: true
|
||||||
|
mipmap: false
|
||||||
|
|
||||||
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSourceSizeChanged: {
|
||||||
|
if (!root.generateThumbnail) return;
|
||||||
|
thumbnailGeneration.running = false;
|
||||||
|
thumbnailGeneration.running = true;
|
||||||
|
}
|
||||||
|
Process {
|
||||||
|
id: thumbnailGeneration
|
||||||
|
command: {
|
||||||
|
const maxSize = Images.thumbnailSizes[root.thumbnailSizeName];
|
||||||
|
return ["bash", "-c",
|
||||||
|
`[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 1) { // Force reload if thumbnail had to be generated
|
||||||
|
root.source = "";
|
||||||
|
root.source = root.thumbnailPath; // Force reload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,20 @@ Singleton {
|
|||||||
return trimmed.split(/[\\/]/).pop();
|
return trimmed.split(/[\\/]/).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the folder name from a directory path
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function folderNameForPath(str) {
|
||||||
|
if (typeof str !== "string") return "";
|
||||||
|
const trimmed = trimFileProtocol(str);
|
||||||
|
// Remove trailing slash if present
|
||||||
|
const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
||||||
|
if (!noTrailing) return "";
|
||||||
|
return noTrailing.split(/[\\/]/).pop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the file extension from a file path or name
|
* Removes the file extension from a file path or name
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
@@ -38,4 +52,18 @@ Singleton {
|
|||||||
}
|
}
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parent directory of a given file path
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function parentDirectory(str) {
|
||||||
|
if (typeof str !== "string") return "";
|
||||||
|
const trimmed = trimFileProtocol(str);
|
||||||
|
const parts = trimmed.split(/[\\/]/);
|
||||||
|
if (parts.length <= 1) return "";
|
||||||
|
parts.pop();
|
||||||
|
return parts.join("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
required property var directory
|
||||||
|
property bool showBreadcrumb: true
|
||||||
|
onShowBreadcrumbChanged: {
|
||||||
|
addressInput.text = root.directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal navigateToDirectory(string path)
|
||||||
|
|
||||||
|
property real padding: 6
|
||||||
|
implicitWidth: mainLayout.implicitWidth + padding * 2
|
||||||
|
implicitHeight: mainLayout.implicitHeight + padding * 2
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
|
||||||
|
function focusBreadcrumb() {
|
||||||
|
root.showBreadcrumb = false;
|
||||||
|
addressInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: mainLayout
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: root.padding
|
||||||
|
}
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
id: parentDirButton
|
||||||
|
onClicked: root.navigateToDirectory(FileUtils.parentDirectory(root.directory))
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
text: "drive_folder_upload"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: directoryEntry
|
||||||
|
visible: !root.showBreadcrumb
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
implicitWidth: addressInput.implicitWidth
|
||||||
|
implicitHeight: addressInput.implicitHeight
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (directoryEntry.visible && event.key === Qt.Key_Escape) {
|
||||||
|
root.showBreadcrumb = true;
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledTextInput {
|
||||||
|
id: addressInput
|
||||||
|
anchors.fill: parent
|
||||||
|
padding: 10
|
||||||
|
text: root.directory
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.navigateToDirectory(text);
|
||||||
|
root.showBreadcrumb = true;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
// I-beam cursor
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: breadcrumbLoader
|
||||||
|
active: root.showBreadcrumb
|
||||||
|
visible: root.showBreadcrumb
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: AddressBreadcrumb {
|
||||||
|
directory: root.directory
|
||||||
|
onNavigateToDirectory: dir => {
|
||||||
|
root.navigateToDirectory(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
id: dirEditButton
|
||||||
|
toggled: !root.showBreadcrumb
|
||||||
|
onClicked: root.showBreadcrumb = !root.showBreadcrumb
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
text: "edit"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledToolTip {
|
||||||
|
content: Translation.tr("Edit directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: root
|
||||||
|
required property var directory
|
||||||
|
property var breadcrumbDirectory: ""
|
||||||
|
Component.onCompleted: breadcrumbDirectory = directory;
|
||||||
|
onDirectoryChanged: {
|
||||||
|
if (breadcrumbDirectory.startsWith(directory)) return;
|
||||||
|
breadcrumbDirectory = directory
|
||||||
|
}
|
||||||
|
|
||||||
|
signal navigateToDirectory(string path)
|
||||||
|
|
||||||
|
orientation: ListView.Horizontal
|
||||||
|
clip: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
model: breadcrumbDirectory.split("/")
|
||||||
|
delegate: SelectionGroupButton {
|
||||||
|
id: folderButton
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
buttonText: index === 0 ? "/" : modelData
|
||||||
|
toggled: {
|
||||||
|
if (directory.trim() === "/") return index === 0;
|
||||||
|
return index === directory.split("/").length - 1
|
||||||
|
}
|
||||||
|
leftmost: index === 0
|
||||||
|
rightmost: index === breadcrumbDirectory.split("/").length - 1
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].some(dir => FileUtils.trimFileProtocol(dir) === fileModelData.filePath))
|
||||||
|
return Quickshell.iconPath(`folder-${fileModelData.fileName.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
ScrollBar {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
|
contentItem: Rectangle {
|
||||||
|
implicitWidth: 4
|
||||||
|
implicitHeight: root.visualSize
|
||||||
|
radius: width / 2
|
||||||
|
color: Appearance.colors.colOnSurfaceVariant
|
||||||
|
|
||||||
|
opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 350
|
||||||
|
easing.type: Appearance.animation.elementMoveFast.type
|
||||||
|
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import QtQuick.Controls
|
|||||||
* Does not include visual layout, but includes the easily neglected colors.
|
* Does not include visual layout, but includes the easily neglected colors.
|
||||||
*/
|
*/
|
||||||
TextInput {
|
TextInput {
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
selectionColor: Appearance.colors.colSecondaryContainer
|
selectionColor: Appearance.colors.colSecondaryContainer
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
required property var fileModelData
|
||||||
|
property bool isDirectory: fileModelData.fileIsDir
|
||||||
|
property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName)
|
||||||
|
|
||||||
|
property alias colBackground: background.color
|
||||||
|
property alias colText: wallpaperItemName.color
|
||||||
|
property alias radius: background.radius
|
||||||
|
property alias margins: background.anchors.margins
|
||||||
|
property alias padding: wallpaperItemColumnLayout.anchors.margins
|
||||||
|
margins: Appearance.sizes.wallpaperSelectorItemMargins
|
||||||
|
padding: Appearance.sizes.wallpaperSelectorItemPadding
|
||||||
|
|
||||||
|
signal activated()
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: root.activated()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
Behavior on color {
|
||||||
|
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: wallpaperItemColumnLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wallpaperItemImageContainer
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
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.useThumbnail
|
||||||
|
sourceComponent: ThumbnailImage {
|
||||||
|
id: thumbnailImage
|
||||||
|
generateThumbnail: false
|
||||||
|
sourcePath: fileModelData.filePath
|
||||||
|
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
clip: true
|
||||||
|
sourceSize.width: wallpaperItemColumnLayout.width
|
||||||
|
sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Wallpapers
|
||||||
|
function onThumbnailGenerated(directory) {
|
||||||
|
if (thumbnailImage.status !== Image.Error) return;
|
||||||
|
if (FileUtils.parentDirectory(thumbnailImage.sourcePath) !== directory) return;
|
||||||
|
thumbnailImage.source = "";
|
||||||
|
thumbnailImage.source = thumbnailImage.thumbnailPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: wallpaperItemImageContainer.width
|
||||||
|
height: wallpaperItemImageContainer.height
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
id: wallpaperItemName
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 10
|
||||||
|
Layout.rightMargin: 10
|
||||||
|
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||||
|
Behavior on color {
|
||||||
|
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
text: fileModelData.fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: wallpaperSelectorLoader
|
||||||
|
active: GlobalStates.wallpaperSelectorOpen
|
||||||
|
|
||||||
|
sourceComponent: PanelWindow {
|
||||||
|
id: panelWindow
|
||||||
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
|
||||||
|
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
|
||||||
|
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.namespace: "quickshell:wallpaperSelector"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
margins {
|
||||||
|
top: Config?.options.bar.vertical ? Appearance.sizes.hyprlandGapsOut : Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: content
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: Appearance.sizes.wallpaperSelectorHeight
|
||||||
|
implicitWidth: Appearance.sizes.wallpaperSelectorWidth
|
||||||
|
|
||||||
|
HyprlandFocusGrab { // Click outside to close
|
||||||
|
id: grab
|
||||||
|
windows: [ panelWindow ]
|
||||||
|
active: wallpaperSelectorLoader.active
|
||||||
|
onCleared: () => {
|
||||||
|
if (!active) GlobalStates.wallpaperSelectorOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperSelectorContent {
|
||||||
|
id: content
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "wallpaperSelector"
|
||||||
|
|
||||||
|
function toggle(): void {
|
||||||
|
GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalShortcut {
|
||||||
|
name: "wallpaperSelectorToggle"
|
||||||
|
description: "Toggle wallpaper selector"
|
||||||
|
onPressed: {
|
||||||
|
GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
property int columns: 4
|
||||||
|
property real previewCellAspectRatio: 4 / 3
|
||||||
|
property bool useDarkMode: Appearance.m3colors.darkmode
|
||||||
|
|
||||||
|
function updateThumbnails() {
|
||||||
|
const totalImageMargin = (Appearance.sizes.wallpaperSelectorItemMargins + Appearance.sizes.wallpaperSelectorItemPadding) * 2
|
||||||
|
const thumbnailSizeName = Images.thumbnailSizeNameForDimensions(grid.cellWidth - totalImageMargin, grid.cellHeight - totalImageMargin)
|
||||||
|
Wallpapers.generateThumbnail(thumbnailSizeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Wallpapers
|
||||||
|
function onDirectoryChanged() {
|
||||||
|
root.updateThumbnails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
GlobalStates.wallpaperSelectorOpen = false;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.modifiers & Qt.AltModifier && event.key === Qt.Key_Up) {
|
||||||
|
Wallpapers.setDirectory(FileUtils.parentDirectory(Wallpapers.directory));
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Left) {
|
||||||
|
grid.moveSelection(-1);
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Right) {
|
||||||
|
grid.moveSelection(1);
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
grid.moveSelection(-grid.columns);
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Down) {
|
||||||
|
grid.moveSelection(grid.columns);
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
grid.activateCurrent();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Backspace) {
|
||||||
|
if (filterField.text.length > 0) {
|
||||||
|
filterField.text = filterField.text.substring(0, filterField.text.length - 1);
|
||||||
|
}
|
||||||
|
filterField.forceActiveFocus();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) {
|
||||||
|
addressBar.focusBreadcrumb();
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Slash) {
|
||||||
|
filterField.forceActiveFocus();
|
||||||
|
event.accepted = true;
|
||||||
|
} else {
|
||||||
|
if (event.text.length > 0) {
|
||||||
|
filterField.text += event.text;
|
||||||
|
filterField.cursorPosition = filterField.text.length;
|
||||||
|
filterField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: mainLayout.implicitHeight
|
||||||
|
implicitWidth: mainLayout.implicitWidth
|
||||||
|
|
||||||
|
StyledRectangularShadow {
|
||||||
|
target: wallpaperGridBackground
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: wallpaperGridBackground
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: Appearance.sizes.elevationMargin
|
||||||
|
}
|
||||||
|
focus: true
|
||||||
|
border.width: 1
|
||||||
|
border.color: Appearance.colors.colLayer0Border
|
||||||
|
color: Appearance.colors.colLayer0
|
||||||
|
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||||
|
|
||||||
|
property int calculatedRows: Math.ceil(grid.count / grid.columns)
|
||||||
|
|
||||||
|
implicitWidth: gridColumnLayout.implicitWidth
|
||||||
|
implicitHeight: gridColumnLayout.implicitHeight
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: mainLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: -4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 4
|
||||||
|
implicitWidth: quickDirColumnLayout.implicitWidth
|
||||||
|
implicitHeight: quickDirColumnLayout.implicitHeight
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: wallpaperGridBackground.radius - Layout.margins
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: quickDirColumnLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.margins: 12
|
||||||
|
font {
|
||||||
|
pixelSize: Appearance.font.pixelSize.normal
|
||||||
|
weight: Font.Medium
|
||||||
|
}
|
||||||
|
text: Translation.tr("Pick a wallpaper")
|
||||||
|
}
|
||||||
|
ListView {
|
||||||
|
// Quick dirs
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 4
|
||||||
|
implicitWidth: 140
|
||||||
|
clip: true
|
||||||
|
model: [
|
||||||
|
{ icon: "home", name: "Home", path: Directories.home },
|
||||||
|
{ icon: "docs", 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
|
||||||
|
required property var modelData
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
onClicked: Wallpapers.setDirectory(quickDirButton.modelData.path)
|
||||||
|
enabled: modelData.icon.length > 0
|
||||||
|
toggled: Wallpapers.directory === FileUtils.trimFileProtocol(modelData.path)
|
||||||
|
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||||
|
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||||
|
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
MaterialSymbol {
|
||||||
|
color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
text: quickDirButton.modelData.icon
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
text: quickDirButton.modelData.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: gridColumnLayout
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
AddressBar {
|
||||||
|
id: addressBar
|
||||||
|
Layout.margins: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: false
|
||||||
|
directory: Wallpapers.directory
|
||||||
|
onNavigateToDirectory: path => {
|
||||||
|
Wallpapers.setDirectory(path.length == 0 ? "/" : path);
|
||||||
|
}
|
||||||
|
radius: wallpaperGridBackground.radius - Layout.margins
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: gridDisplayRegion
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: gridDisplayRegion.width
|
||||||
|
height: gridDisplayRegion.height
|
||||||
|
radius: wallpaperGridBackground.radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: grid
|
||||||
|
visible: Wallpapers.folderModel.count > 0
|
||||||
|
|
||||||
|
readonly property int columns: root.columns
|
||||||
|
readonly property int rows: Math.max(1, Math.ceil(count / columns))
|
||||||
|
property int currentIndex: 0
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
cellWidth: width / root.columns
|
||||||
|
cellHeight: cellWidth / root.previewCellAspectRatio
|
||||||
|
interactive: true
|
||||||
|
clip: true
|
||||||
|
keyNavigationWraps: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
bottomMargin: extraOptions.implicitHeight
|
||||||
|
ScrollBar.vertical: StyledScrollBar {}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.updateThumbnails()
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSelection(delta) {
|
||||||
|
currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta));
|
||||||
|
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateCurrent() {
|
||||||
|
const filePath = grid.model.get(currentIndex, "filePath")
|
||||||
|
Wallpapers.select(filePath, root.useDarkMode);
|
||||||
|
filterField.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
model: Wallpapers.folderModel
|
||||||
|
onModelChanged: currentIndex = 0
|
||||||
|
delegate: WallpaperDirectoryItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
fileModelData: modelData
|
||||||
|
width: grid.cellWidth
|
||||||
|
height: grid.cellHeight
|
||||||
|
colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colSecondaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer)
|
||||||
|
colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colOnPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer0
|
||||||
|
|
||||||
|
onEntered: {
|
||||||
|
grid.currentIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated: {
|
||||||
|
Wallpapers.select(fileModelData.filePath, root.useDarkMode);
|
||||||
|
filterField.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: extraOptions
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2
|
||||||
|
implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2
|
||||||
|
|
||||||
|
StyledRectangularShadow {
|
||||||
|
target: extraOptionsBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { // Bottom toolbar
|
||||||
|
id: extraOptionsBackground
|
||||||
|
property real padding: 6
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: 8
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2
|
||||||
|
implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: extraOptionsRowLayout
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: extraOptionsBackground.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
implicitWidth: height
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
onClicked: {
|
||||||
|
Wallpapers.openFallbackPicker(root.useDarkMode);
|
||||||
|
GlobalStates.wallpaperSelectorOpen = false;
|
||||||
|
}
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
text: "open_in_new"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
StyledToolTip {
|
||||||
|
content: Translation.tr("Use the system file picker instead")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
implicitWidth: height
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
onClicked: root.useDarkMode = !root.useDarkMode
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
text: root.useDarkMode ? "dark_mode" : "light_mode"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
StyledToolTip {
|
||||||
|
content: Translation.tr("Click to toggle light/dark mode (applied when wallpaper is chosen)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: filterField
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
implicitWidth: 200
|
||||||
|
padding: 10
|
||||||
|
placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search")
|
||||||
|
placeholderTextColor: Appearance.colors.colSubtext
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
selectionColor: Appearance.colors.colSecondaryContainer
|
||||||
|
background: Rectangle {
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
Wallpapers.searchQuery = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (text.length !== 0) {
|
||||||
|
// No filtering, just navigate grid
|
||||||
|
if (event.key === Qt.Key_Down) {
|
||||||
|
grid.moveSelection(grid.columns);
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_Up) {
|
||||||
|
grid.moveSelection(-grid.columns);
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 2
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
onClicked: {
|
||||||
|
GlobalStates.wallpaperSelectorOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GlobalStates
|
||||||
|
function onWallpaperSelectorOpenChanged() {
|
||||||
|
if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) {
|
||||||
|
filterField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Wallpapers
|
||||||
|
function onChanged() {
|
||||||
|
GlobalStates.wallpaperSelectorOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
|
||||||
|
|
||||||
|
# From https://github.com/difference-engine/thumbnail-generator-ubuntu (MIT License)
|
||||||
|
# Since the script is small and the maintainers seem inactive to accept my PR (#11) I decided to just copy it over.
|
||||||
|
# When it gets merged and the python package gets updated we can just use it
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from multiprocessing import Pool
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
import click
|
||||||
|
import gi
|
||||||
|
from loguru import logger
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
gi.require_version("GnomeDesktop", "3.0")
|
||||||
|
from gi.repository import Gio, GnomeDesktop # isort:skip
|
||||||
|
|
||||||
|
thumbnail_size_map = {
|
||||||
|
"normal": GnomeDesktop.DesktopThumbnailSize.NORMAL,
|
||||||
|
"large": GnomeDesktop.DesktopThumbnailSize.LARGE,
|
||||||
|
"x-large": GnomeDesktop.DesktopThumbnailSize.XLARGE,
|
||||||
|
"xx-large": GnomeDesktop.DesktopThumbnailSize.XXLARGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
factory = None
|
||||||
|
logger.remove()
|
||||||
|
logger.add(sys.stdout, level="INFO")
|
||||||
|
logger.add("/tmp/thumbgen.log", level="DEBUG", rotation="100 MB")
|
||||||
|
|
||||||
|
def make_thumbnail(fpath: str) -> bool:
|
||||||
|
mtime = os.path.getmtime(fpath)
|
||||||
|
# Use Gio to determine the URI and mime type
|
||||||
|
f = Gio.file_new_for_path(str(fpath))
|
||||||
|
uri = f.get_uri()
|
||||||
|
info = f.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
|
||||||
|
mime_type = info.get_content_type()
|
||||||
|
|
||||||
|
if factory.lookup(uri, mtime) is not None:
|
||||||
|
logger.debug("FRESH {}".format(uri))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not factory.can_thumbnail(uri, mime_type, mtime):
|
||||||
|
logger.debug("UNSUPPORTED {}".format(uri))
|
||||||
|
return False
|
||||||
|
|
||||||
|
thumbnail = factory.generate_thumbnail(uri, mime_type)
|
||||||
|
if thumbnail is None:
|
||||||
|
logger.debug("ERROR {}".format(uri))
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.debug("OK {}".format(uri))
|
||||||
|
factory.save_thumbnail(thumbnail, uri, mtime)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch()
|
||||||
|
def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool) -> None:
|
||||||
|
all_files = get_all_files(dir_path=dir_path, recursive=recursive)
|
||||||
|
if only_images:
|
||||||
|
all_files = get_all_images(all_files=all_files)
|
||||||
|
all_files = [str(fpath) for fpath in all_files]
|
||||||
|
with Pool(processes=workers) as p:
|
||||||
|
list(tqdm(p.imap(make_thumbnail, all_files), total=len(all_files)))
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_images(*, all_files: List[Path]) -> List[Path]:
|
||||||
|
img_suffixes = [".jpg", ".jpeg", ".png", ".gif"]
|
||||||
|
all_images = [fpath for fpath in all_files if fpath.suffix in img_suffixes]
|
||||||
|
print("Found {} images".format(len(all_images)))
|
||||||
|
return all_images
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_files(*, dir_path: Path, recursive: bool) -> List[Path]:
|
||||||
|
if not (dir_path.exists() and dir_path.is_dir()):
|
||||||
|
raise ValueError("{} doesn't exist or isn't a valid directory!".format(dir_path.resolve()))
|
||||||
|
if recursive:
|
||||||
|
all_files = dir_path.rglob("*")
|
||||||
|
else:
|
||||||
|
all_files = dir_path.glob("*")
|
||||||
|
all_files = [fpath for fpath in all_files if fpath.is_file()]
|
||||||
|
print("Found {} files in the directory: {}".format(len(all_files), dir_path.resolve()))
|
||||||
|
return all_files
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"-d", "--img_dirs", required=True, help='directories to generate thumbnails seperated by space, eg: "dir1/dir2 dir3"'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-s", "--size", default="normal", type=click.Choice(["normal", "large", "x-large", "xx-large"]), help="Thumbnail size: normal, large, x-large, xx-large"
|
||||||
|
)
|
||||||
|
@click.option("-w", "--workers", default=1, help="no of cpus to use for processing")
|
||||||
|
@click.option(
|
||||||
|
"-i", "--only_images", is_flag=True, default=False, help="Whether to only look for images to be thumbnailed"
|
||||||
|
)
|
||||||
|
@click.option("-r", "--recursive", is_flag=True, default=False, help="Whether to recursively look for files")
|
||||||
|
def main(img_dirs: str, size: str, workers: str, only_images: bool, recursive: bool) -> None:
|
||||||
|
img_dirs = [Path(img_dir) for img_dir in img_dirs.split()]
|
||||||
|
global factory
|
||||||
|
factory = GnomeDesktop.DesktopThumbnailFactory.new(thumbnail_size_map[size])
|
||||||
|
for img_dir in img_dirs:
|
||||||
|
thumbnail_folder(dir_path=img_dir, workers=workers, only_images=only_images, recursive=recursive)
|
||||||
|
print("Thumbnail Generation Completed!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import QtQuick
|
||||||
|
import Qt.labs.folderlistmodel
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a list of wallpapers and an "apply" action that calls the existing
|
||||||
|
* switchwall.sh script. Pretty much a limited file browsing service.
|
||||||
|
*/
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/thumbgen.py`
|
||||||
|
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
|
||||||
|
"jpg", "jpeg", "png", "webp", "avif", "bmp", "svg"
|
||||||
|
]
|
||||||
|
property list<string> wallpapers: [] // List of absolute file paths (without file://)
|
||||||
|
|
||||||
|
signal changed()
|
||||||
|
signal thumbnailGenerated(directory: string)
|
||||||
|
|
||||||
|
// Executions
|
||||||
|
Process {
|
||||||
|
id: applyProc
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFallbackPicker(darkMode = Appearance.m3colors.darkmode) {
|
||||||
|
applyProc.exec([
|
||||||
|
Directories.wallpaperSwitchScriptPath,
|
||||||
|
"--mode", (darkMode ? "dark" : "light")
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(path, darkMode = Appearance.m3colors.darkmode) {
|
||||||
|
if (!path || path.length === 0) return
|
||||||
|
applyProc.exec([
|
||||||
|
Directories.wallpaperSwitchScriptPath,
|
||||||
|
"--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, darkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: validateDirProc
|
||||||
|
property string nicePath: ""
|
||||||
|
function setDirectoryIfValid(path) {
|
||||||
|
validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "")
|
||||||
|
if (/^\/*$/.test(validateDirProc.nicePath)) validateDirProc.nicePath = "/";
|
||||||
|
validateDirProc.exec(["test", "-d", nicePath])
|
||||||
|
}
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.directory = validateDirProc.nicePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setDirectory(path) {
|
||||||
|
validateDirProc.setDirectoryIfValid(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder model
|
||||||
|
FolderListModel {
|
||||||
|
id: folderModel
|
||||||
|
folder: Qt.resolvedUrl(root.directory)
|
||||||
|
caseSensitive: false
|
||||||
|
nameFilters: root.extensions.map(ext => `*${searchQuery.split(" ").filter(s => s.length > 0).map(s => `*${s}*`)}*.${ext}`)
|
||||||
|
showDirs: true
|
||||||
|
showDotAndDotDot: false
|
||||||
|
showOnlyReadable: true
|
||||||
|
sortField: FolderListModel.Time
|
||||||
|
sortReversed: false
|
||||||
|
onCountChanged: {
|
||||||
|
root.wallpapers = []
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail generation
|
||||||
|
function generateThumbnail(size: string) {
|
||||||
|
if (!["normal", "large", "x-large", "xx-large"].includes(size)) throw new Error("Invalid thumbnail size");
|
||||||
|
thumbgenProc.directory = root.directory
|
||||||
|
thumbgenProc.running = false
|
||||||
|
thumbgenProc.command = [
|
||||||
|
thumbgenScriptPath,
|
||||||
|
"--size", size,
|
||||||
|
"-d", `${root.directory}`
|
||||||
|
]
|
||||||
|
thumbgenProc.running = true
|
||||||
|
}
|
||||||
|
Process {
|
||||||
|
id: thumbgenProc
|
||||||
|
property string directory
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
root.thumbnailGenerated(thumbgenProc.directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import "./modules/sessionScreen/"
|
|||||||
import "./modules/sidebarLeft/"
|
import "./modules/sidebarLeft/"
|
||||||
import "./modules/sidebarRight/"
|
import "./modules/sidebarRight/"
|
||||||
import "./modules/verticalBar/"
|
import "./modules/verticalBar/"
|
||||||
|
import "./modules/wallpaperSelector/"
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
@@ -49,6 +50,7 @@ ShellRoot {
|
|||||||
property bool enableSidebarLeft: true
|
property bool enableSidebarLeft: true
|
||||||
property bool enableSidebarRight: true
|
property bool enableSidebarRight: true
|
||||||
property bool enableVerticalBar: true
|
property bool enableVerticalBar: true
|
||||||
|
property bool enableWallpaperSelector: true
|
||||||
|
|
||||||
// Force initialization of some singletons
|
// Force initialization of some singletons
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -76,5 +78,6 @@ ShellRoot {
|
|||||||
LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
|
LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
|
||||||
LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
|
LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
|
||||||
LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} }
|
LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} }
|
||||||
|
LazyLoader { active: enableWallpaperSelector; component: WallpaperSelector {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,8 @@ materialyoucolor
|
|||||||
libsass
|
libsass
|
||||||
material-color-utilities
|
material-color-utilities
|
||||||
setproctitle
|
setproctitle
|
||||||
|
click
|
||||||
|
loguru
|
||||||
|
pycairo
|
||||||
|
pygobject
|
||||||
|
tqdm
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ build==1.2.2.post1
|
|||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
# via pywayland
|
# via pywayland
|
||||||
|
click==8.2.1
|
||||||
|
# via -r scriptdata/requirements.in
|
||||||
libsass==0.23.0
|
libsass==0.23.0
|
||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
|
loguru==0.7.3
|
||||||
|
# via -r scriptdata/requirements.in
|
||||||
material-color-utilities==0.2.1
|
material-color-utilities==0.2.1
|
||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
materialyoucolor==2.0.10
|
materialyoucolor==2.0.10
|
||||||
@@ -22,8 +26,14 @@ pillow==11.1.0
|
|||||||
# material-color-utilities
|
# material-color-utilities
|
||||||
psutil==6.1.1
|
psutil==6.1.1
|
||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
|
pycairo==1.28.0
|
||||||
|
# via
|
||||||
|
# -r scriptdata/requirements.in
|
||||||
|
# pygobject
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
|
pygobject==3.52.3
|
||||||
|
# via -r scriptdata/requirements.in
|
||||||
pyproject-hooks==1.2.0
|
pyproject-hooks==1.2.0
|
||||||
# via build
|
# via build
|
||||||
pywayland==0.4.18
|
pywayland==0.4.18
|
||||||
@@ -34,5 +44,7 @@ setuptools==80.9.0
|
|||||||
# via setuptools-scm
|
# via setuptools-scm
|
||||||
setuptools-scm==8.1.0
|
setuptools-scm==8.1.0
|
||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
|
tqdm==4.67.1
|
||||||
|
# via -r scriptdata/requirements.in
|
||||||
wheel==0.45.1
|
wheel==0.45.1
|
||||||
# via -r scriptdata/requirements.in
|
# via -r scriptdata/requirements.in
|
||||||
|
|||||||
Reference in New Issue
Block a user