forked from Shinonome/dots-hyprland
feat: add wallpaper selector menu (#1820)
This commit is contained in:
@@ -350,6 +350,10 @@ Singleton {
|
||||
property real baseVerticalBarWidth: 46
|
||||
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
|
||||
(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"
|
||||
|
||||
@@ -8,12 +8,17 @@ import Quickshell
|
||||
|
||||
Singleton {
|
||||
// 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 state: StandardPaths.standardLocations(StandardPaths.StateLocation)[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 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://"
|
||||
property string assetsPath: Quickshell.shellPath("assets")
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {string} str
|
||||
@@ -38,4 +52,18 @@ Singleton {
|
||||
}
|
||||
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.
|
||||
*/
|
||||
TextInput {
|
||||
color: Appearance.colors.colOnLayer1
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
|
||||
Reference in New Issue
Block a user