wallpaper selector: add address bar

This commit is contained in:
end-4 2025-08-23 21:06:36 +07:00
parent 18ad260ce9
commit 8e6582b801
6 changed files with 444 additions and 269 deletions

View 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
}
}
}
}

View file

@ -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("/");
}
} }

View file

@ -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("/"))
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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