mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-24 12:22:09 +00:00
wallpaper selector: add address bar
This commit is contained in:
parent
18ad260ce9
commit
8e6582b801
6 changed files with 444 additions and 269 deletions
94
.config/quickshell/ii/modules/common/AddressBar.qml
Normal file
94
.config/quickshell/ii/modules/common/AddressBar.qml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
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 // TODO: make this work
|
||||||
|
|
||||||
|
signal navigateToDirectory(string path)
|
||||||
|
|
||||||
|
property real padding: 6
|
||||||
|
implicitWidth: mainLayout.implicitWidth + padding * 2
|
||||||
|
implicitHeight: mainLayout.implicitHeight + padding * 2
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: !root.showBreadcrumb
|
||||||
|
visible: !root.showBreadcrumb
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: Rectangle {
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
implicitWidth: addressInput.implicitWidth
|
||||||
|
implicitHeight: addressInput.implicitHeight
|
||||||
|
|
||||||
|
StyledTextInput {
|
||||||
|
id: addressInput
|
||||||
|
anchors.fill: parent
|
||||||
|
padding: 10
|
||||||
|
text: root.directory
|
||||||
|
|
||||||
|
onAccepted: root.navigateToDirectory(text)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
// I-beam cursor
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,38 @@
|
||||||
|
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: index === directory.split("/").length - 1
|
||||||
|
leftmost: index === 0
|
||||||
|
rightmost: index === breadcrumbDirectory.split("/").length - 1
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,6 @@ Scope {
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
WlrLayershell.namespace: "quickshell:wallpaperSelector"
|
WlrLayershell.namespace: "quickshell:wallpaperSelector"
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
anchors.top: true
|
anchors.top: true
|
||||||
|
|
|
||||||
|
|
@ -86,59 +86,62 @@ Item {
|
||||||
border.color: Appearance.colors.colLayer0Border
|
border.color: Appearance.colors.colLayer0Border
|
||||||
color: Appearance.colors.colLayer0
|
color: Appearance.colors.colLayer0
|
||||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: wallpaperGridBackground.width
|
|
||||||
height: wallpaperGridBackground.height
|
|
||||||
radius: wallpaperGridBackground.radius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property int calculatedRows: Math.ceil(grid.count / grid.columns)
|
property int calculatedRows: Math.ceil(grid.count / grid.columns)
|
||||||
|
|
||||||
// implicitWidth: gridColumnLayout.implicitWidth
|
// implicitWidth: gridColumnLayout.implicitWidth
|
||||||
// implicitHeight: gridColumnLayout.implicitHeight
|
// implicitHeight: gridColumnLayout.implicitHeight
|
||||||
|
|
||||||
Item {
|
ColumnLayout {
|
||||||
// The grid
|
// The grid
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
AddressBar {
|
||||||
|
id: addressBar
|
||||||
|
Layout.margins: 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: false
|
||||||
|
directory: Wallpapers.directory
|
||||||
|
onNavigateToDirectory: path => {
|
||||||
|
Wallpapers.directory = 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 {
|
GridView {
|
||||||
id: grid
|
id: grid
|
||||||
visible: root.wallpapers.length > 0
|
visible: root.wallpapers.length > 0
|
||||||
|
|
||||||
property int currentIndex: 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))
|
||||||
|
property int currentIndex: 0
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cellWidth: width / root.columns
|
cellWidth: width / root.columns
|
||||||
cellHeight: cellWidth / root.previewCellAspectRatio
|
cellHeight: cellWidth / root.previewCellAspectRatio
|
||||||
clip: true
|
|
||||||
interactive: true
|
interactive: true
|
||||||
|
clip: true
|
||||||
keyNavigationWraps: true
|
keyNavigationWraps: true
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
bottomMargin: extraOptions.implicitHeight
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
policy: ScrollBar.AsNeeded
|
policy: ScrollBar.AsNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
let filtered = root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase())));
|
|
||||||
// Add 'columns' empty entries to the end
|
|
||||||
for (let i = 0; i < root.columns; i++) {
|
|
||||||
filtered.push("");
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onModelChanged: currentIndex = 0
|
|
||||||
|
|
||||||
function moveSelection(delta) {
|
function moveSelection(delta) {
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const item = itemAtIndex(i);
|
const item = itemAtIndex(i);
|
||||||
|
|
@ -149,6 +152,7 @@ Item {
|
||||||
currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta));
|
currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta));
|
||||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateCurrent() {
|
function activateCurrent() {
|
||||||
const path = model[currentIndex];
|
const path = model[currentIndex];
|
||||||
if (!path)
|
if (!path)
|
||||||
|
|
@ -158,6 +162,11 @@ Item {
|
||||||
Wallpapers.apply(path);
|
Wallpapers.apply(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase())))
|
||||||
|
}
|
||||||
|
onModelChanged: currentIndex = 0
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: wallpaperItem
|
id: wallpaperItem
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
@ -174,6 +183,9 @@ Item {
|
||||||
}
|
}
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
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)
|
||||||
|
Behavior on color {
|
||||||
|
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: wallpaperItemColumnLayout
|
id: wallpaperItemColumnLayout
|
||||||
|
|
@ -240,10 +252,14 @@ Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 10
|
Layout.leftMargin: 10
|
||||||
Layout.rightMargin: 10
|
Layout.rightMargin: 10
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||||
color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0
|
color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0
|
||||||
|
Behavior on color {
|
||||||
|
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||||
|
}
|
||||||
text: FileUtils.fileNameForPath(wallpaperItem.modelData)
|
text: FileUtils.fileNameForPath(wallpaperItem.modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,15 +290,14 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Item {
|
||||||
id: noWallpapersFoundLabel
|
id: extraOptions
|
||||||
visible: grid.model.values.length === 0
|
anchors {
|
||||||
anchors.centerIn: parent
|
bottom: parent.bottom
|
||||||
text: "No wallpapers found"
|
horizontalCenter: parent.horizontalCenter
|
||||||
font.family: Appearance.font.family.main
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
|
||||||
color: Appearance.colors.colSubtext
|
|
||||||
}
|
}
|
||||||
|
implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2
|
||||||
|
implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2
|
||||||
|
|
||||||
StyledRectangularShadow {
|
StyledRectangularShadow {
|
||||||
target: extraOptionsBackground
|
target: extraOptionsBackground
|
||||||
|
|
@ -292,9 +307,8 @@ Item {
|
||||||
id: extraOptionsBackground
|
id: extraOptionsBackground
|
||||||
property real padding: 6
|
property real padding: 6
|
||||||
anchors {
|
anchors {
|
||||||
bottom: parent.bottom
|
fill: parent
|
||||||
horizontalCenter: parent.horizontalCenter
|
margins: 8
|
||||||
bottomMargin: 8
|
|
||||||
}
|
}
|
||||||
color: Appearance.colors.colLayer2
|
color: Appearance.colors.colLayer2
|
||||||
implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2
|
implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2
|
||||||
|
|
@ -412,6 +426,8 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GlobalStates
|
target: GlobalStates
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pragma ComponentBehavior: Bound
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string searchDir: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`)
|
property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`)
|
||||||
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"
|
||||||
]
|
]
|
||||||
|
|
@ -42,7 +42,7 @@ Singleton {
|
||||||
// Folder model
|
// Folder model
|
||||||
FolderListModel {
|
FolderListModel {
|
||||||
id: files
|
id: files
|
||||||
folder: Qt.resolvedUrl(root.searchDir)
|
folder: Qt.resolvedUrl(root.directory)
|
||||||
nameFilters: root.extensions.map(ext => `*.${ext}`)
|
nameFilters: root.extensions.map(ext => `*.${ext}`)
|
||||||
showDirs: false
|
showDirs: false
|
||||||
showDotAndDotDot: false
|
showDotAndDotDot: false
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue