From 28a6284968ddb28c30372eb86f8dabd6f7e04b5d Mon Sep 17 00:00:00 2001 From: sinnayuh Date: Fri, 15 Aug 2025 17:33:10 +0100 Subject: [PATCH 01/68] feat(wallpaper selector): add wallpaper selector menu --- .config/hypr/hyprland/keybinds.conf | 1 + .config/quickshell/ii/GlobalStates.qml | 1 + .../wallpaperOverview/WallpaperOverview.qml | 244 ++++++++++++++++++ .config/quickshell/ii/services/Wallpapers.qml | 76 ++++++ .config/quickshell/ii/shell.qml | 3 + 5 files changed, 325 insertions(+) create mode 100644 .config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml create mode 100644 .config/quickshell/ii/services/Wallpapers.qml diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index c7e953d94..9c9529a69 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -25,6 +25,7 @@ bindit = ,Super_R, global, quickshell:workspaceNumber # [hidden] bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt) +bindd = Super, Comma, Toggle wallpaper overview, global, quickshell:wallpaperOverviewToggle # Wallpaper overview bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] diff --git a/.config/quickshell/ii/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml index 3f7468f4c..57ce3b4cc 100644 --- a/.config/quickshell/ii/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -17,6 +17,7 @@ Singleton { property bool osdVolumeOpen: false property bool oskOpen: false property bool overviewOpen: false + property bool wallpaperOverviewOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml new file mode 100644 index 000000000..233ae9534 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -0,0 +1,244 @@ +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 Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +// Fullscreen panel similar to Overview, but shows a grid of wallpaper previews. +Scope { + id: scope + + Variants { + id: variants + model: Quickshell.screens + + PanelWindow { + id: root + required property var modelData + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + screen: modelData + visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { top: true; bottom: true; left: true; right: true } + + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 + + Item { width: 1; height: 1 } + + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + // Compact size for 4 thumbnails per row, 3 rows high + implicitWidth: Math.min(root.width * 0.7, 900) + implicitHeight: Math.min(root.height * 0.6, 500) + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false + 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 + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + GridView { + id: grid + readonly property int columns: 4 // Fixed to 4 columns + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + cellWidth: 220 + cellHeight: 140 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + // Performance optimization: cache more delegates for smoother scrolling + cacheBuffer: cellHeight * 2 // Cache 2 extra rows above/below visible area + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + // Back to simple wallpapers array + model: Wallpapers.wallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + // Clear all hover states when using keyboard navigation + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i) + if (item) { + item.isHovered = false + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)) + positionViewAtIndex(currentIndex, GridView.Contain) + } + function activateCurrent() { + const path = Wallpapers.wallpapers[currentIndex] + if (!path) return + GlobalStates.wallpaperOverviewOpen = false + Wallpapers.apply(path) + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + // Loading placeholder + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + // Simple loading animation + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800; easing.type: Easing.InOutSine } + NumberAnimation { to: 0.3; duration: 800; easing.type: Easing.InOutSine } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: `file://${modelData}` + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + // Much smaller sourceSize for faster loading - this is key! + // Using smaller dimensions significantly reduces decode time + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + // Disable mipmap for faster loading (quality vs speed tradeoff) + mipmap: false + + // Smooth fade-in when ready + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + // Clear all other hover states and set current index + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i) + if (item && item !== parent) { + item.isHovered = false + } + } + parent.isHovered = true + grid.currentIndex = index + } + onExited: { + parent.isHovered = false + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false + Wallpapers.apply(modelData) + } + } + } + } + } + } + } + + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + bg.forceActiveFocus(); + } + } + } + } + } + + GlobalShortcut { + name: "wallpaperOverviewToggle" + description: "Toggle wallpaper overview" + onPressed: { GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen } + } +} + + diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml new file mode 100644 index 000000000..d2db55141 --- /dev/null +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -0,0 +1,76 @@ +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. Uses QML's built-in image scaling for thumbnails. + */ +Singleton { + id: root + + // Directory to search for wallpapers (new location) + // Resolves to: ~/Pictures/wallpapers + property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/wallpapers`) ] + + // Supported image extensions. Videos are intentionally excluded for now + // to keep the overview lightweight. + readonly property list extensions: [ + "jpg", "jpeg", "png", "webp", "avif", "bmp" + ] + + // Resulting list of absolute file paths (without file:// prefix) + property list wallpapers: [] + + // Public API (FolderListModel driven) + function reload() { + files.folder = `file://${root.searchDirs[0]}` + } + onSearchDirsChanged: reload() + + function apply(path) { + if (!path || path.length === 0) return + applyProc.command = [ + "bash", "-c", + `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + + `--image ${StringUtils.shellSingleQuoteEscape(path)}` + ] + applyProc.running = true + } + + // Folder model + FolderListModel { + id: files + nameFilters: extensions.map(ext => `*.${ext}`) + showDirs: false + showDotAndDotDot: false + showOnlyReadable: true + sortField: FolderListModel.Time + sortReversed: true + onCountChanged: { + console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) + root.wallpapers = [] + for (let i = 0; i < files.count; i++) { + const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) + if (path && path.length) root.wallpapers.push(path) + } + } + } + + // Expose the model for direct binding when needed + property alias filesModel: files + + // Internal: applying a wallpaper + Process { + id: applyProc + } + + Component.onCompleted: reload() +} + + diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index 8186d1a56..45532738c 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -18,6 +18,7 @@ import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/onScreenKeyboard/" import "./modules/overview/" +import "./modules/wallpaperOverview/" import "./modules/screenCorners/" import "./modules/session/" import "./modules/sidebarLeft/" @@ -43,6 +44,7 @@ ShellRoot { property bool enableOnScreenDisplayVolume: true property bool enableOnScreenKeyboard: true property bool enableOverview: true + property bool enableWallpaperOverview: true property bool enableReloadPopup: true property bool enableScreenCorners: true property bool enableSession: true @@ -69,6 +71,7 @@ ShellRoot { LazyLoader { active: enableOnScreenDisplayVolume; component: OnScreenDisplayVolume {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOverview; component: Overview {} } + LazyLoader { active: enableWallpaperOverview; component: WallpaperOverview {} } LazyLoader { active: enableReloadPopup; component: ReloadPopup {} } LazyLoader { active: enableScreenCorners; component: ScreenCorners {} } LazyLoader { active: enableSession; component: Session {} } From 190dbff98d70536e25f0819ab3e2056183847206 Mon Sep 17 00:00:00 2001 From: sin <92541374+sinnayuh@users.noreply.github.com> Date: Sun, 17 Aug 2025 10:51:51 +0000 Subject: [PATCH 02/68] feat(wallpaper selector): add search functionality --- .../wallpaperOverview/WallpaperOverview.qml | 328 ++++++++++++++---- 1 file changed, 257 insertions(+), 71 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 233ae9534..ef1fa2d03 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -11,7 +11,6 @@ import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland -// Fullscreen panel similar to Overview, but shows a grid of wallpaper previews. Scope { id: scope @@ -19,20 +18,26 @@ Scope { id: variants model: Quickshell.screens - PanelWindow { - id: root - required property var modelData - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - screen: modelData - visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + PanelWindow { + id: root + required property var modelData + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + screen: modelData + visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + property var filteredWallpapers: Wallpapers.wallpapers WlrLayershell.namespace: "quickshell:wallpaper-overview" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" - anchors { top: true; bottom: true; left: true; right: true } + anchors { + top: true + bottom: true + left: true + right: true + } ColumnLayout { id: layout @@ -40,7 +45,76 @@ Scope { anchors.top: parent.top spacing: 8 - Item { width: 1; height: 1 } + Item { + width: 1 + height: 1 + } + + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; + } + event.accepted = true; + } + } + } Rectangle { id: bg @@ -50,29 +124,76 @@ Scope { border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding - // Compact size for 4 thumbnails per row, 3 rows high - implicitWidth: Math.min(root.width * 0.7, 900) - implicitHeight: Math.min(root.height * 0.6, 500) + property int calculatedRows: Math.ceil(grid.count / grid.columns) - Keys.onPressed: (event) => { + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); + } + } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Behavior on implicitHeight { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false - event.accepted = true + GlobalStates.wallpaperOverviewOpen = false; + event.accepted = true; } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1) - event.accepted = true + grid.moveSelection(-1); + event.accepted = true; } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1) - event.accepted = true + grid.moveSelection(1); + event.accepted = true; } else if (event.key === Qt.Key_Up) { - grid.moveSelection(-grid.columns) - event.accepted = true + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns) - event.accepted = true + grid.moveSelection(grid.columns); + event.accepted = true; } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent() - event.accepted = true + 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 { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } } @@ -83,12 +204,14 @@ Scope { GridView { id: grid - readonly property int columns: 4 // Fixed to 4 columns + visible: root.filteredWallpapers.length > 0 + + readonly property int columns: 4 property int currentIndex: 0 readonly property int rows: Math.max(1, Math.ceil(count / columns)) Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHcenter Layout.fillHeight: true cellWidth: 220 cellHeight: 140 @@ -96,37 +219,37 @@ Scope { interactive: true keyNavigationWraps: true boundsBehavior: Flickable.StopAtBounds - - // Performance optimization: cache more delegates for smoother scrolling - cacheBuffer: cellHeight * 2 // Cache 2 extra rows above/below visible area - ScrollBar.horizontal: ScrollBar { + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded visible: false } - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded visible: false } - // Back to simple wallpapers array - model: Wallpapers.wallpapers + + model: root.filteredWallpapers onModelChanged: currentIndex = 0 function moveSelection(delta) { - // Clear all hover states when using keyboard navigation for (let i = 0; i < count; i++) { - const item = itemAtIndex(i) + const item = itemAtIndex(i); if (item) { - item.isHovered = false + item.isHovered = false; } } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)) - positionViewAtIndex(currentIndex, GridView.Contain) + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); } function activateCurrent() { - const path = Wallpapers.wallpapers[currentIndex] - if (!path) return - GlobalStates.wallpaperOverviewOpen = false - Wallpapers.apply(path) + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); } delegate: Item { @@ -134,6 +257,20 @@ Scope { height: grid.cellHeight property bool isHovered: false + Behavior on width { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Behavior on height { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + Rectangle { anchors.fill: parent radius: Appearance.rounding.windowRounding @@ -147,8 +284,7 @@ Scope { anchors.margins: 8 color: Appearance.colors.colLayer2 radius: Appearance.rounding.elementRounding - - // Loading placeholder + Rectangle { anchors.centerIn: parent width: Math.min(parent.width * 0.4, 32) @@ -156,14 +292,21 @@ Scope { radius: Appearance.rounding.elementRounding color: Appearance.colors.colLayer3 visible: thumbnailImage.status !== Image.Ready - - // Simple loading animation + opacity: 0.3 SequentialAnimation on opacity { running: parent.visible loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 800; easing.type: Easing.InOutSine } - NumberAnimation { to: 0.3; duration: 800; easing.type: Easing.InOutSine } + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } } } @@ -175,21 +318,17 @@ Scope { asynchronous: true cache: false smooth: true - - // Much smaller sourceSize for faster loading - this is key! - // Using smaller dimensions significantly reduces decode time + sourceSize.width: Math.min(128, grid.cellWidth - 16) sourceSize.height: Math.min(96, grid.cellHeight - 16) - - // Disable mipmap for faster loading (quality vs speed tradeoff) + mipmap: false - - // Smooth fade-in when ready + opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { - NumberAnimation { + NumberAnimation { duration: 200 - easing.type: Easing.OutCubic + easing.type: Easing.OutCubic } } } @@ -199,25 +338,72 @@ Scope { anchors.fill: parent hoverEnabled: true onEntered: { - // Clear all other hover states and set current index for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i) + const item = grid.itemAtIndex(i); if (item && item !== parent) { - item.isHovered = false + item.isHovered = false; } } - parent.isHovered = true - grid.currentIndex = index + parent.isHovered = true; + grid.currentIndex = index; } onExited: { - parent.isHovered = false + parent.isHovered = false; } onClicked: { - GlobalStates.wallpaperOverviewOpen = false - Wallpapers.apply(modelData) + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } } } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent + + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + } } } } @@ -227,7 +413,7 @@ Scope { target: GlobalStates function onWallpaperOverviewOpenChanged() { if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - bg.forceActiveFocus(); + filterField.forceActiveFocus(); } } } @@ -237,8 +423,8 @@ Scope { GlobalShortcut { name: "wallpaperOverviewToggle" description: "Toggle wallpaper overview" - onPressed: { GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen } + onPressed: { + GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen; + } } } - - From 5b1124d65899e787e850818e39994785c03a9996 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 17 Aug 2025 21:50:12 +0700 Subject: [PATCH 03/68] correct wallpaper path capitalization --- .config/quickshell/ii/services/Wallpapers.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d2db55141..aeead2795 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,8 +15,8 @@ Singleton { id: root // Directory to search for wallpapers (new location) - // Resolves to: ~/Pictures/wallpapers - property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/wallpapers`) ] + // Resolves to: ~/Pictures/Wallpapers + property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) ] // Supported image extensions. Videos are intentionally excluded for now // to keep the overview lightweight. From de6ba1c82e1db5d2279257ba123d36b1f6ef3e1e Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:54:40 +0700 Subject: [PATCH 04/68] wallpapers: add svg format --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index aeead2795..e7d244426 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -21,7 +21,7 @@ Singleton { // Supported image extensions. Videos are intentionally excluded for now // to keep the overview lightweight. readonly property list extensions: [ - "jpg", "jpeg", "png", "webp", "avif", "bmp" + "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] // Resulting list of absolute file paths (without file:// prefix) From b01b52c8f86f55c642c822171d6fa455c15a4ed5 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:24:41 +0700 Subject: [PATCH 05/68] wallpapers: show newer ones first --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index e7d244426..4683bed99 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -51,7 +51,7 @@ Singleton { showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time - sortReversed: true + sortReversed: false onCountChanged: { console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) root.wallpapers = [] From fe23017a97faec7d5cf44b4f65d64bd67c71fb0e Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:25:23 +0700 Subject: [PATCH 06/68] WallpaperOverview: remove unnecessary Variants and Item --- .../wallpaperOverview/WallpaperOverview.qml | 752 +++++++++--------- 1 file changed, 370 insertions(+), 382 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index ef1fa2d03..7ca666b18 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -14,408 +14,396 @@ import Quickshell.Hyprland Scope { id: scope - Variants { - id: variants - model: Quickshell.screens + PanelWindow { + id: root + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + visible: GlobalStates.wallpaperOverviewOpen + property var filteredWallpapers: Wallpapers.wallpapers - PanelWindow { - id: root - required property var modelData - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - screen: modelData - visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused - property var filteredWallpapers: Wallpapers.wallpapers + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" - WlrLayershell.namespace: "quickshell:wallpaper-overview" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - color: "transparent" + anchors { + top: true + bottom: true + left: true + right: true + } - anchors { - top: true - bottom: true - left: true - right: true - } + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 - - Item { - width: 1 - height: 1 - } - - TextField { - id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter - implicitHeight: 40 - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - root.filteredWallpapers = newModel; - } else { - root.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperOverviewOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { color: Appearance.colors.colLayer0 - border.width: 1 border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(root.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - SpringAnimation { - spring: 3 - damping: 0.2 - } - } - - Behavior on implicitHeight { - SpringAnimation { - spring: 3 - damping: 0.2 - } - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; - 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) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); event.accepted = true; - } else if (event.key === Qt.Key_Down) { + } + } 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 { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; + bg.forceActiveFocus(); } } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 - - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHcenter - Layout.fillHeight: true - cellWidth: 220 - cellHeight: 140 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Behavior on width { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - - Behavior on height { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding - - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: `file://${modelData}` - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); - } - } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - } - } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter - } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; } + event.accepted = true; } } } - Connections { - target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - filterField.forceActiveFocus(); + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); } } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Behavior on implicitHeight { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false; + 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) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + 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 { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + GridView { + id: grid + visible: root.filteredWallpapers.length > 0 + + readonly property int columns: 4 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHcenter + Layout.fillHeight: true + cellWidth: 220 + cellHeight: 140 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + + model: root.filteredWallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Behavior on width { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Behavior on height { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: `file://${modelData}` + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } + } + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent + + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + } + } + } + } + } + + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } } } } From 8feee4e61a0b06832ae894e499a4ec72c90a4993 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:27:54 +0700 Subject: [PATCH 07/68] WallpaperOverview: make anims consistent --- .../wallpaperOverview/WallpaperOverview.qml | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 7ca666b18..70d065dba 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -134,17 +134,11 @@ Scope { } Behavior on implicitWidth { - SpringAnimation { - spring: 3 - damping: 0.2 - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Behavior on implicitHeight { - SpringAnimation { - spring: 3 - damping: 0.2 - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Keys.onPressed: event => { @@ -247,17 +241,11 @@ Scope { property bool isHovered: false Behavior on width { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Behavior on height { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Rectangle { @@ -315,10 +303,7 @@ Scope { opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } } From 445b10d6f04f732e80c2e6ac51115fed173e6da1 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:41:44 +0700 Subject: [PATCH 08/68] put wallpaper picker in loader and make it use real thumbnails --- .../ii/modules/common/Directories.qml | 1 + .../wallpaperOverview/WallpaperOverview.qml | 658 +++++++++--------- 2 files changed, 334 insertions(+), 325 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index a1748ece3..2b235c691 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -11,6 +11,7 @@ Singleton { 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 genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 70d065dba..667f2015e 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -14,380 +14,388 @@ import Quickshell.Hyprland Scope { id: scope - PanelWindow { - id: root - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - visible: GlobalStates.wallpaperOverviewOpen - property var filteredWallpapers: Wallpapers.wallpapers + Loader { + active: GlobalStates.wallpaperOverviewOpen + sourceComponent: PanelWindow { + id: root + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + property var filteredWallpapers: Wallpapers.wallpapers - WlrLayershell.namespace: "quickshell:wallpaper-overview" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - color: "transparent" + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" - anchors { - top: true - bottom: true - left: true - right: true - } + anchors { + top: true + bottom: true + left: true + right: true + } - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 - TextField { - id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter - implicitHeight: 40 - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); } } - root.filteredWallpapers = newModel; - } else { - root.filteredWallpapers = Wallpapers.wallpapers; + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; + } + event.accepted = true; + } } } - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); } - } else { - if (event.key === Qt.Key_Down) { + } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false; + 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) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperOverviewOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(root.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; - 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) { - if (grid.currentIndex < grid.columns) { + } 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 { - grid.moveSelection(-grid.columns); + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } - 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 { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; } - } - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 + GridView { + id: grid + visible: root.filteredWallpapers.length > 0 - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + readonly property int columns: 4 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHcenter - Layout.fillHeight: true - cellWidth: 220 - cellHeight: 140 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + cellWidth: 128 + cellHeight: 72 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 + model: root.filteredWallpapers + onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Behavior on width { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); } - Behavior on height { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } + Behavior on width { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding + Behavior on height { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage anchors.fill: parent - source: `file://${modelData}` - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } } - onClicked: { - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter } } } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter - } - } } } - } - Connections { - target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - filterField.forceActiveFocus(); + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } } } } From 2d8eb163e784b44f9ac39c60da7b41f91eb0bf69 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:55:54 +0700 Subject: [PATCH 09/68] rename wallpaper overview to wallpaper selector --- .config/hypr/hyprland/keybinds.conf | 4 ++-- .config/quickshell/ii/GlobalStates.qml | 2 +- .../WallpaperSelector.qml} | 23 ++++++++++--------- .config/quickshell/ii/shell.qml | 6 ++--- 4 files changed, 18 insertions(+), 17 deletions(-) rename .config/quickshell/ii/modules/{wallpaperOverview/WallpaperOverview.qml => wallpaperSelector/WallpaperSelector.qml} (96%) diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index ca723b8df..e29dcaba5 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -25,7 +25,6 @@ bindit = ,Super_R, global, quickshell:workspaceNumber # [hidden] bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt) -bindd = Super, Comma, Toggle wallpaper overview, global, quickshell:wallpaperOverviewToggle # Wallpaper overview bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] @@ -49,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 = ,XF86AudioMicMute, 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 ##! Utilities diff --git a/.config/quickshell/ii/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml index 57ce3b4cc..f2836c99b 100644 --- a/.config/quickshell/ii/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -17,7 +17,7 @@ Singleton { property bool osdVolumeOpen: false property bool oskOpen: false property bool overviewOpen: false - property bool wallpaperOverviewOpen: false + property bool wallpaperSelectorOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml similarity index 96% rename from .config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml rename to .config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 667f2015e..645a125c6 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -15,14 +15,15 @@ Scope { id: scope Loader { - active: GlobalStates.wallpaperOverviewOpen + active: GlobalStates.wallpaperSelectorOpen + sourceComponent: PanelWindow { id: root readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) property var filteredWallpapers: Wallpapers.wallpapers - WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" @@ -99,7 +100,7 @@ Scope { if (filterField.text.length > 0) { filterField.text = ""; } else { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; } event.accepted = true; } @@ -144,7 +145,7 @@ Scope { Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; event.accepted = true; } else if (event.key === Qt.Key_Left) { grid.moveSelection(-1); @@ -231,7 +232,7 @@ Scope { const path = model[currentIndex]; if (!path) return; - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; Wallpapers.apply(path); } @@ -332,7 +333,7 @@ Scope { parent.isHovered = false; } onClicked: { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; Wallpapers.apply(modelData); } @@ -392,8 +393,8 @@ Scope { Connections { target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + function onwallpaperSelectorOpenChanged() { + if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { filterField.forceActiveFocus(); } } @@ -402,10 +403,10 @@ Scope { } GlobalShortcut { - name: "wallpaperOverviewToggle" - description: "Toggle wallpaper overview" + name: "wallpaperSelectorToggle" + description: "Toggle wallpaper selector" onPressed: { - GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen; + GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen; } } } diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index b135f5924..56dc75ff0 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -18,12 +18,12 @@ import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/onScreenKeyboard/" import "./modules/overview/" -import "./modules/wallpaperOverview/" import "./modules/screenCorners/" import "./modules/session/" import "./modules/sidebarLeft/" import "./modules/sidebarRight/" import "./modules/verticalBar/" +import "./modules/wallpaperSelector/" import QtQuick import QtQuick.Window @@ -44,13 +44,13 @@ ShellRoot { property bool enableOnScreenDisplayVolume: true property bool enableOnScreenKeyboard: true property bool enableOverview: true - property bool enableWallpaperOverview: true property bool enableReloadPopup: true property bool enableScreenCorners: true property bool enableSession: true property bool enableSidebarLeft: true property bool enableSidebarRight: true property bool enableVerticalBar: true + property bool enableWallpaperSelector: true // Force initialization of some singletons Component.onCompleted: { @@ -72,12 +72,12 @@ ShellRoot { LazyLoader { active: enableOnScreenDisplayVolume; component: OnScreenDisplayVolume {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOverview; component: Overview {} } - LazyLoader { active: enableWallpaperOverview; component: WallpaperOverview {} } LazyLoader { active: enableReloadPopup; component: ReloadPopup {} } LazyLoader { active: enableScreenCorners; component: ScreenCorners {} } LazyLoader { active: enableSession; component: Session {} } LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} } LazyLoader { active: enableSidebarRight; component: SidebarRight {} } LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} } + LazyLoader { active: enableWallpaperSelector; component: WallpaperSelector {} } } From 6aa37e252978f7041ae9e95f571a544b48e410df Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:59:26 +0700 Subject: [PATCH 10/68] wallpaper selector: dont cache images --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 645a125c6..4704c8887 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -301,7 +301,7 @@ Scope { } fillMode: Image.PreserveAspectCrop asynchronous: true - cache: true + cache: false smooth: true sourceSize.width: Math.min(128, grid.cellWidth - 16) From 28fe7817b470ff575bf79b8324d821b1107e5066 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:00:41 +0700 Subject: [PATCH 11/68] wallpaper selector: add ipchandler alongside globalshortcut --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 4704c8887..6642d20e6 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -402,6 +402,14 @@ Scope { } } + IpcHandler { + target: "wallpaperSelector" + + function toggle(): void { + GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen + } + } + GlobalShortcut { name: "wallpaperSelectorToggle" description: "Toggle wallpaper selector" From 42695d9253df2568c181c2fdd6b7d1963591375b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:02:03 +0700 Subject: [PATCH 12/68] Wallpapers: use Process::exec instead of setting command and setting `running` prop --- .config/quickshell/ii/services/Wallpapers.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 4683bed99..d5c4e0701 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -35,12 +35,11 @@ Singleton { function apply(path) { if (!path || path.length === 0) return - applyProc.command = [ + applyProc.exec([ "bash", "-c", `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + `--image ${StringUtils.shellSingleQuoteEscape(path)}` - ] - applyProc.running = true + ]) } // Folder model From 89e726b5a2d7d22501b79df98ebb9d411ad9b76a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:09:10 +0700 Subject: [PATCH 13/68] remove animations that aren't supposed to happen --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 6642d20e6..91a9dbc04 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -242,14 +242,6 @@ Scope { height: grid.cellHeight property bool isHovered: false - Behavior on width { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on height { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - Rectangle { anchors.fill: parent radius: Appearance.rounding.windowRounding From 8124f688da5c347ebe120a88d3d25cbfbd5caed8 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:35:01 +0700 Subject: [PATCH 14/68] remove manual `file://` construction --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d5c4e0701..98b78b0b6 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -29,7 +29,7 @@ Singleton { // Public API (FolderListModel driven) function reload() { - files.folder = `file://${root.searchDirs[0]}` + files.folder = Qt.resolvedUrl(root.searchDirs[0]) } onSearchDirsChanged: reload() From 980533052bc7c89b09e38e3340c240dc992bf1e7 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:47:55 +0700 Subject: [PATCH 15/68] wallpaper selector: remove some useless layout crap --- .../wallpaperSelector/WallpaperSelector.qml | 356 +++++++++--------- 1 file changed, 173 insertions(+), 183 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 91a9dbc04..effd880ce 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -12,17 +12,20 @@ import Quickshell.Wayland import Quickshell.Hyprland Scope { - id: scope + id: root + property int thumbnailWidth: 128 + property int thumbnailHeight: 72 Loader { active: GlobalStates.wallpaperSelectorOpen sourceComponent: PanelWindow { - id: root - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + id: panelWindow + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) property var filteredWallpapers: Wallpapers.wallpapers + exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand @@ -35,6 +38,10 @@ Scope { right: true } + margins { + top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut + } + ColumnLayout { id: layout anchors.horizontalCenter: parent.horizontalCenter @@ -43,9 +50,9 @@ Scope { TextField { id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter + Layout.alignment: Qt.AlignHCenter implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth padding: 10 placeholderText: "Search wallpapers..." placeholderTextColor: Appearance.colors.colSubtext @@ -68,9 +75,9 @@ Scope { newModel.push(wallpaperPath); } } - root.filteredWallpapers = newModel; + panelWindow.filteredWallpapers = newModel; } else { - root.filteredWallpapers = Wallpapers.wallpapers; + panelWindow.filteredWallpapers = Wallpapers.wallpapers; } } @@ -114,24 +121,25 @@ Scope { border.width: 1 border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding + // Layout.alignment: Qt.AlignHCenter property int calculatedRows: Math.ceil(grid.count / grid.columns) implicitWidth: { - if (root.filteredWallpapers.length === 0) { + if (panelWindow.filteredWallpapers.length === 0) { return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; } else { - return Math.min(root.width * 0.7, 900); + return Math.min(panelWindow.width * 0.7, 900); } } implicitHeight: { - if (root.filteredWallpapers.length === 0) { + if (panelWindow.filteredWallpapers.length === 0) { return 100; } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); } } @@ -182,204 +190,186 @@ Scope { } } - ColumnLayout { + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 + + readonly property int columns: 7 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + anchors.fill: parent - anchors.margins: 8 - spacing: 8 + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - cellWidth: 128 - cellHeight: 72 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary } - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine } } } - MouseArea { + Image { + id: thumbnailImage anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); } + parent.isHovered = true; + grid.currentIndex = index; } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } } } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } + + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } } } From 9cc576b98de946188bde842e2ac7c02825bbf387 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:58:13 +0700 Subject: [PATCH 16/68] wallpaper selector: move content to a new file --- .../wallpaperSelector/WallpaperSelector.qml | 343 +---------------- .../WallpaperSelectorContent.qml | 354 ++++++++++++++++++ 2 files changed, 358 insertions(+), 339 deletions(-) create mode 100644 .config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index effd880ce..c3ff54cd9 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -13,8 +13,6 @@ import Quickshell.Hyprland Scope { id: root - property int thumbnailWidth: 128 - property int thumbnailHeight: 72 Loader { active: GlobalStates.wallpaperSelectorOpen @@ -42,343 +40,10 @@ Scope { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 - - TextField { - id: filterField - Layout.alignment: Qt.AlignHCenter - implicitHeight: 40 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - // Layout.alignment: Qt.AlignHCenter - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { - return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } - } - - implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - 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) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - 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 { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - - GridView { - id: grid - visible: panelWindow.filteredWallpapers.length > 0 - - readonly property int columns: 7 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - - model: panelWindow.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding - - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); - } - } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - } - } - - Label { - id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } - } - } - - Connections { - target: GlobalStates - function onwallpaperSelectorOpenChanged() { - if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { - filterField.forceActiveFocus(); - } + WallpaperSelectorContent { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter } } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml new file mode 100644 index 000000000..84a7e9611 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -0,0 +1,354 @@ +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 +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { + id: root + property int columns: 4 + property int thumbnailWidth: 192 + property int thumbnailHeight: 108 + implicitHeight: columnLayout.implicitHeight + implicitWidth: columnLayout.implicitWidth + + ColumnLayout { + id: columnLayout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 + + TextField { + id: filterField + Layout.alignment: Qt.AlignHCenter + implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + panelWindow.filteredWallpapers = newModel; + } else { + panelWindow.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + // Layout.alignment: Qt.AlignHCenter + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (panelWindow.filteredWallpapers.length === 0) { + return 300; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(panelWindow.width * 0.7, 900); + } + } + + implicitHeight: { + if (panelWindow.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + 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) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + 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 { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } + + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 + + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small + + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height + radius: Appearance.rounding.small + } + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.width: 1 + border.color: Appearance.colors.colOutlineVariant + radius: Appearance.rounding.small + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } + } + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } + } + } + + Connections { + target: GlobalStates + function onWallpaperSelectorOpenChanged() { + if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } + } + } + +} \ No newline at end of file From dac9ed27850308f3740b334c001b6ea27a39d9cc Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:03:12 +0700 Subject: [PATCH 17/68] wallpaper selector: add shadows --- .../WallpaperSelectorContent.qml | 559 +++++++++--------- 1 file changed, 288 insertions(+), 271 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 84a7e9611..bd2be581a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -19,326 +19,344 @@ Item { property int thumbnailHeight: 108 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth - + ColumnLayout { id: columnLayout anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top spacing: 8 - TextField { - id: filterField + Item { + implicitHeight: filterField.implicitHeight + implicitWidth: filterField.implicitWidth Layout.alignment: Qt.AlignHCenter - implicitHeight: 40 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); + StyledRectangularShadow { + target: filterField + } + + TextField { + id: filterField + implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + panelWindow.filteredWallpapers = newModel; + } else { + panelWindow.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); } } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } event.accepted = true; } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; } } } - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - // Layout.alignment: Qt.AlignHCenter - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { - return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } + Item { + implicitWidth: wallpaperGridBackground.implicitWidth + implicitHeight: wallpaperGridBackground.implicitHeight + + StyledRectangularShadow { + target: wallpaperGridBackground } + Rectangle { + id: wallpaperGridBackground - implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } + Layout.alignment: Qt.AlignHCenter + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding + border.width: 1 + border.color: Appearance.colors.colLayer0Border + focus: true - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + property int calculatedRows: Math.ceil(grid.count / grid.columns) - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - 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) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); + implicitWidth: { + if (panelWindow.filteredWallpapers.length === 0) { + return 300; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; } else { - grid.moveSelection(-grid.columns); + return Math.min(panelWindow.width * 0.7, 900); } - 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 { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - - GridView { - id: grid - visible: panelWindow.filteredWallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded } - model: panelWindow.filteredWallpapers - onModelChanged: currentIndex = 0 + implicitHeight: { + if (panelWindow.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + 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) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + 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 { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); } - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded } - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.small + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 - Image { - id: thumbnailImage + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height + radius: Appearance.rounding.small + } + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.width: 1 + border.color: Appearance.colors.colOutlineVariant radius: Appearance.rounding.small } } - - Rectangle { - anchors.fill: parent - color: "transparent" - border.width: 1 - border.color: Appearance.colors.colOutlineVariant - radius: Appearance.rounding.small - } } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext } } - - Label { - id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } } } @@ -350,5 +368,4 @@ Item { } } } - -} \ No newline at end of file +} From 28256c0a720330a6e2fc9a5e98c4e138f5f49f1d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:03:41 +0700 Subject: [PATCH 18/68] wallpaper selector: add shadows --- .../modules/wallpaperSelector/WallpaperSelectorContent.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index bd2be581a..981d7a43a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -102,16 +102,15 @@ Item { } Item { + Layout.alignment: Qt.AlignHCenter implicitWidth: wallpaperGridBackground.implicitWidth implicitHeight: wallpaperGridBackground.implicitHeight - + StyledRectangularShadow { target: wallpaperGridBackground } Rectangle { id: wallpaperGridBackground - - Layout.alignment: Qt.AlignHCenter color: Appearance.colors.colLayer0 radius: Appearance.rounding.screenRounding border.width: 1 From a25a3c186b9603bed8e390183c6725c8868512ab Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:47:21 +0700 Subject: [PATCH 19/68] wallpaper selector: add click outside to close --- .../wallpaperSelector/WallpaperSelector.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index c3ff54cd9..2fee98fdb 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -15,6 +15,7 @@ Scope { id: root Loader { + id: wallpaperSelectorLoader active: GlobalStates.wallpaperSelectorOpen sourceComponent: PanelWindow { @@ -35,12 +36,25 @@ Scope { left: true right: true } - margins { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } + mask: Region { + item: content + } + + HyprlandFocusGrab { // Click outside to close + id: grab + windows: [ panelWindow ] + active: wallpaperSelectorLoader.active + onCleared: () => { + if (!active) GlobalStates.wallpaperSelectorOpen = false; + } + } + WallpaperSelectorContent { + id: content anchors { top: parent.top horizontalCenter: parent.horizontalCenter From 767e35851b018ed087de9cec202ae262a39ee752 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 09:36:43 +0700 Subject: [PATCH 20/68] wallpaper selector: move key handling to root --- .../wallpaperSelector/WallpaperSelector.qml | 2 - .../WallpaperSelectorContent.qml | 134 +++++++++--------- 2 files changed, 66 insertions(+), 70 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 2fee98fdb..0a9abab5f 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -5,7 +5,6 @@ import qs.modules.common.widgets import qs.modules.common.functions import QtQuick import QtQuick.Controls -import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland @@ -22,7 +21,6 @@ Scope { id: panelWindow readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - property var filteredWallpapers: Wallpapers.wallpapers exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 981d7a43a..670fcabce 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,10 +15,51 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property int thumbnailWidth: 192 - property int thumbnailHeight: 108 + property int thumbnailWidth: 256 + property int thumbnailHeight: 144 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth + property var filteredWallpapers: Wallpapers.wallpapers + property string filterQuery: "" + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + 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) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + 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 { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } ColumnLayout { id: columnLayout @@ -26,7 +67,7 @@ Item { anchors.top: parent.top spacing: 8 - Item { + Item { // Search box implicitHeight: filterField.implicitHeight implicitWidth: filterField.implicitWidth Layout.alignment: Qt.AlignHCenter @@ -53,24 +94,13 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } + root.filterQuery = text } Keys.onPressed: event => { if (text.length === 0) { if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); + wallpaperGrid.forceActiveFocus(); if (event.key === Qt.Key_Down) grid.moveSelection(grid.columns); else if (event.key === Qt.Key_Left) @@ -83,7 +113,7 @@ Item { if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; - bg.forceActiveFocus(); + wallpaperGrid.forceActiveFocus(); } } if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { @@ -101,7 +131,8 @@ Item { } } - Item { + Item { // The grid + id: wallpaperGrid Layout.alignment: Qt.AlignHCenter implicitWidth: wallpaperGridBackground.implicitWidth implicitHeight: wallpaperGridBackground.implicitHeight @@ -120,17 +151,17 @@ Item { property int calculatedRows: Math.ceil(grid.count / grid.columns) implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { + if (root.filteredWallpapers.length === 0) { return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; } else { return Math.min(panelWindow.width * 0.7, 900); } } implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { + if (root.filteredWallpapers.length === 0) { return 100; } else { return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); @@ -145,48 +176,9 @@ Item { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - 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) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - 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 { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - GridView { id: grid - visible: panelWindow.filteredWallpapers.length > 0 + visible: root.filteredWallpapers.length > 0 property int currentIndex: 0 readonly property int columns: root.columns @@ -208,7 +200,13 @@ Item { policy: ScrollBar.AsNeeded } - model: panelWindow.filteredWallpapers + model: ScriptModel { + values: { + return root.filteredWallpapers.filter(w => ( + w.toLowerCase().includes(root.filterQuery.toLowerCase()) + )); + } + } onModelChanged: currentIndex = 0 function moveSelection(delta) { @@ -332,15 +330,15 @@ Item { property: "scale" from: 0.0 to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial + duration: Appearance.animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial } NumberAnimation { property: "opacity" from: 0.0 to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial + duration: Appearance.animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial } } } @@ -348,7 +346,7 @@ Item { Label { id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 + visible: root.filteredWallpapers.length === 0 anchors.centerIn: parent text: "No wallpapers found" font.family: Appearance.font.family.main From 0e2eea7555813fe77e6b8c558aab06c87021de88 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 10:13:52 +0700 Subject: [PATCH 21/68] wallpaper selector: unfuck grid placement --- .../ii/modules/common/Appearance.qml | 2 + .../wallpaperSelector/WallpaperSelector.qml | 13 +- .../WallpaperSelectorContent.qml | 198 +++++++----------- 3 files changed, 81 insertions(+), 132 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index 870bbfe36..e1f0853a5 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -343,6 +343,8 @@ Singleton { property real baseVerticalBarWidth: 46 property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ? (baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth + property real wallpaperSelectorWidth: 1000 + property real wallpaperSelectorHeight: 580 } syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light" diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 0a9abab5f..76923705f 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -28,12 +28,7 @@ Scope { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" - anchors { - top: true - bottom: true - left: true - right: true - } + anchors.top: true margins { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } @@ -42,6 +37,9 @@ Scope { item: content } + implicitHeight: Appearance.sizes.wallpaperSelectorHeight + implicitWidth: Appearance.sizes.wallpaperSelectorWidth + HyprlandFocusGrab { // Click outside to close id: grab windows: [ panelWindow ] @@ -54,8 +52,7 @@ Scope { WallpaperSelectorContent { id: content anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter + fill: parent } } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 670fcabce..52a75658b 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,8 +15,7 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property int thumbnailWidth: 256 - property int thumbnailHeight: 144 + property real previewAspectRatio: 16 / 9 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth property var filteredWallpapers: Wallpapers.wallpapers @@ -63,13 +62,13 @@ Item { ColumnLayout { id: columnLayout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 + anchors.fill: parent + spacing: -Appearance.sizes.elevationMargin - Item { // Search box - implicitHeight: filterField.implicitHeight - implicitWidth: filterField.implicitWidth + Item { + // Search box + implicitHeight: filterField.implicitHeight + Appearance.sizes.elevationMargin * 2 + implicitWidth: filterField.implicitWidth + Appearance.sizes.elevationMargin * 2 Layout.alignment: Qt.AlignHCenter StyledRectangularShadow { @@ -78,7 +77,11 @@ Item { TextField { id: filterField - implicitHeight: 40 + anchors { + fill: parent + margins: Appearance.sizes.elevationMargin + } + implicitHeight: 44 implicitWidth: Appearance.sizes.searchWidth padding: 10 placeholderText: "Search wallpapers..." @@ -94,7 +97,7 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal onTextChanged: { - root.filterQuery = text + root.filterQuery = text; } Keys.onPressed: event => { @@ -133,48 +136,38 @@ Item { Item { // The grid id: wallpaperGrid - Layout.alignment: Qt.AlignHCenter - implicitWidth: wallpaperGridBackground.implicitWidth - implicitHeight: wallpaperGridBackground.implicitHeight + Layout.fillWidth: true + Layout.fillHeight: true + implicitWidth: wallpaperGridBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: wallpaperGridBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 StyledRectangularShadow { target: wallpaperGridBackground } Rectangle { id: wallpaperGridBackground - color: Appearance.colors.colLayer0 - radius: Appearance.rounding.screenRounding + anchors { + fill: parent + margins: Appearance.sizes.elevationMargin + } + focus: true border.width: 1 border.color: Appearance.colors.colLayer0Border - focus: true + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding + 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) - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + implicitWidth: grid.implicitWidth + implicitHeight: grid.implicitHeight GridView { id: grid @@ -185,8 +178,8 @@ Item { readonly property int rows: Math.max(1, Math.ceil(count / columns)) anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewAspectRatio clip: true interactive: true keyNavigationWraps: true @@ -202,9 +195,7 @@ Item { model: ScriptModel { values: { - return root.filteredWallpapers.filter(w => ( - w.toLowerCase().includes(root.filterQuery.toLowerCase()) - )); + return root.filteredWallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); } } onModelChanged: currentIndex = 0 @@ -233,61 +224,49 @@ Item { height: grid.cellHeight property bool isHovered: false - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } + Image { + id: thumbnailImage + anchors { + fill: parent + margins: 8 + } + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.small + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height - radius: Appearance.rounding.small - } - } - - Rectangle { - anchors.fill: parent - color: "transparent" - border.width: 1 - border.color: Appearance.colors.colOutlineVariant + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height radius: Appearance.rounding.small } } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Appearance.rounding.small + border.width: (index === grid.currentIndex || parent.isHovered) ? 2 : 1 + border.color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colSecondary : Appearance.colors.colOutlineVariant + } } MouseArea { @@ -313,35 +292,6 @@ Item { } } } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: Appearance.animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: Appearance.animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial - } - } - } } Label { From a116ae6ab56bbf4003a525247e3b502f0b0a21c1 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:25:34 +0700 Subject: [PATCH 22/68] wallpaper selector: nicer layout --- .../wallpaperSelector/WallpaperSelector.qml | 2 +- .../WallpaperSelectorContent.qml | 480 +++++++++++------- .config/quickshell/ii/services/Wallpapers.qml | 4 + 3 files changed, 298 insertions(+), 188 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 76923705f..24cad7972 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -30,7 +30,7 @@ Scope { anchors.top: true margins { - top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut + top: Config?.options.bar.vertical ? Appearance.sizes.hyprlandGapsOut : Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } mask: Region { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 52a75658b..c47cf2ef8 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,10 +15,10 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property real previewAspectRatio: 16 / 9 + property real previewCellAspectRatio: 4 / 3 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth - property var filteredWallpapers: Wallpapers.wallpapers + property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" Keys.onPressed: event => { @@ -65,75 +65,6 @@ Item { anchors.fill: parent spacing: -Appearance.sizes.elevationMargin - Item { - // Search box - implicitHeight: filterField.implicitHeight + Appearance.sizes.elevationMargin * 2 - implicitWidth: filterField.implicitWidth + Appearance.sizes.elevationMargin * 2 - Layout.alignment: Qt.AlignHCenter - - StyledRectangularShadow { - target: filterField - } - - TextField { - id: filterField - anchors { - fill: parent - margins: Appearance.sizes.elevationMargin - } - implicitHeight: 44 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - wallpaperGrid.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } - } - } - } - Item { // The grid id: wallpaperGrid Layout.fillWidth: true @@ -154,7 +85,7 @@ Item { border.width: 1 border.color: Appearance.colors.colLayer0Border color: Appearance.colors.colLayer0 - radius: Appearance.rounding.screenRounding + radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { @@ -166,142 +97,317 @@ Item { property int calculatedRows: Math.ceil(grid.count / grid.columns) - implicitWidth: grid.implicitWidth - implicitHeight: grid.implicitHeight - - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + // implicitWidth: gridColumnLayout.implicitWidth + // implicitHeight: gridColumnLayout.implicitHeight + Item { + // The grid anchors.fill: parent - cellWidth: width / root.columns - cellHeight: cellWidth / root.previewAspectRatio - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } + GridView { + id: grid + visible: root.wallpapers.length > 0 - model: ScriptModel { - values: { - return root.filteredWallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewCellAspectRatio + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded } - } - onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + 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; } } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } + onModelChanged: currentIndex = 0 - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Image { - id: thumbnailImage - anchors { - fill: parent - margins: 8 - } - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height - radius: Appearance.rounding.small + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; } } + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + id: wallpaperItem + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false Rectangle { - anchors.fill: parent - color: "transparent" - radius: Appearance.rounding.small - border.width: (index === grid.currentIndex || parent.isHovered) ? 2 : 1 - border.color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colSecondary : Appearance.colors.colOutlineVariant - } - } + anchors { + fill: parent + margins: 8 + } + radius: Appearance.rounding.normal + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 + } + spacing: 4 + + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true + + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } + + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: wallpaperItem.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (wallpaperItem.modelData.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false + + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } + } + } + } + } + + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 + text: FileUtils.fileNameForPath(wallpaperItem.modelData) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(wallpaperItem.modelData); + } } } } - } - Label { - id: noWallpapersFoundLabel - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext + Label { + id: noWallpapersFoundLabel + visible: grid.model.values.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } + + StyledRectangularShadow { + target: extraOptionsBackground + } + + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + bottomMargin: 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 + buttonRadius: Appearance.rounding.full + onClicked: { + Wallpapers.openFallbackPicker(); + GlobalStates.wallpaperSelectorOpen = false; + } + contentItem: RowLayout { + MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger + } + StyledText { + text: Translation.tr("System") + } + } + StyledToolTip { + content: "Use the system file picker instead" + } + } + + TextField { + id: filterField + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: 200 + padding: 10 + placeholderText: Translation.tr("Search wallpapers...") + 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: { + root.filterQuery = text; + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + wallpaperGrid.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + wallpaperGrid.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + } + implicitWidth: height + + contentItem: MaterialSymbol { + text: "close" + iconSize: Appearance.font.pixelSize.larger + } + + StyledToolTip { + content: "Cancel" + } + } + } + } } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 98b78b0b6..ef28e9470 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -33,6 +33,10 @@ Singleton { } onSearchDirsChanged: reload() + function openFallbackPicker() { + applyProc.exec([Directories.wallpaperSwitchScriptPath]) + } + function apply(path) { if (!path || path.length === 0) return applyProc.exec([ From c0933a3b20f99315a46c94621fb9a84453f17d72 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:59:30 +0700 Subject: [PATCH 23/68] wallpaper selector: make it slide from top --- .config/hypr/hyprland/rules.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/hypr/hyprland/rules.conf b/.config/hypr/hyprland/rules.conf index b31b30581..482114f15 100644 --- a/.config/hypr/hyprland/rules.conf +++ b/.config/hypr/hyprland/rules.conf @@ -135,6 +135,7 @@ layerrule = animation slide, quickshell:verticalBar layerrule = animation fade, quickshell:screenCorners layerrule = animation slide right, quickshell:sidebarRight layerrule = animation slide left, quickshell:sidebarLeft +layerrule = animation slide top, quickshell:wallpaperSelector layerrule = animation slide bottom, quickshell:osk layerrule = animation slide bottom, quickshell:dock layerrule = blur, quickshell:session From 18ad260ce93497b9d2dc21288f27a7e3ed316c03 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:59:50 +0700 Subject: [PATCH 24/68] wallpaper service: simplify dir setting --- .config/quickshell/ii/services/Wallpapers.qml | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index ef28e9470..eaa5fd22d 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -9,30 +9,23 @@ pragma ComponentBehavior: Bound /** * Provides a list of wallpapers and an "apply" action that calls the existing - * switchwall.sh script. Uses QML's built-in image scaling for thumbnails. + * switchwall.sh script. Pretty much a limited file browsing service. */ Singleton { id: root - // Directory to search for wallpapers (new location) - // Resolves to: ~/Pictures/Wallpapers - property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) ] - - // Supported image extensions. Videos are intentionally excluded for now - // to keep the overview lightweight. - readonly property list extensions: [ + property string searchDir: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] + property alias filesModel: files // Expose for direct binding when needed + property list wallpapers: [] // List of absolute file paths (without file://) - // Resulting list of absolute file paths (without file:// prefix) - property list wallpapers: [] - - // Public API (FolderListModel driven) - function reload() { - files.folder = Qt.resolvedUrl(root.searchDirs[0]) + // Executions + Process { + id: applyProc } - onSearchDirsChanged: reload() - + function openFallbackPicker() { applyProc.exec([Directories.wallpaperSwitchScriptPath]) } @@ -49,14 +42,14 @@ Singleton { // Folder model FolderListModel { id: files - nameFilters: extensions.map(ext => `*.${ext}`) + folder: Qt.resolvedUrl(root.searchDir) + nameFilters: root.extensions.map(ext => `*.${ext}`) showDirs: false showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time sortReversed: false onCountChanged: { - console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) root.wallpapers = [] for (let i = 0; i < files.count; i++) { const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) @@ -64,16 +57,4 @@ Singleton { } } } - - // Expose the model for direct binding when needed - property alias filesModel: files - - // Internal: applying a wallpaper - Process { - id: applyProc - } - - Component.onCompleted: reload() } - - From 8e6582b8012707ed00aafbc97fce5c30ad691a7a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 21:06:36 +0700 Subject: [PATCH 25/68] wallpaper selector: add address bar --- .../ii/modules/common/AddressBar.qml | 94 +++ .../ii/modules/common/functions/FileUtils.qml | 28 + .../common/widgets/AddressBreadcrumb.qml | 38 ++ .../wallpaperSelector/WallpaperSelector.qml | 1 - .../WallpaperSelectorContent.qml | 548 +++++++++--------- .config/quickshell/ii/services/Wallpapers.qml | 4 +- 6 files changed, 444 insertions(+), 269 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/AddressBar.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml new file mode 100644 index 000000000..b2c1f2bed --- /dev/null +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -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 + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/functions/FileUtils.qml b/.config/quickshell/ii/modules/common/functions/FileUtils.qml index c051674ea..4ed8d8cb1 100644 --- a/.config/quickshell/ii/modules/common/functions/FileUtils.qml +++ b/.config/quickshell/ii/modules/common/functions/FileUtils.qml @@ -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("/"); + } } diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml new file mode 100644 index 000000000..a2d3e0e39 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml @@ -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("/")) + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 24cad7972..0984c85fa 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -25,7 +25,6 @@ Scope { exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" anchors.top: true diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index c47cf2ef8..7ddf4033c 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -86,324 +86,340 @@ Item { border.color: Appearance.colors.colLayer0Border color: Appearance.colors.colLayer0 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) // implicitWidth: gridColumnLayout.implicitWidth // implicitHeight: gridColumnLayout.implicitHeight - Item { + ColumnLayout { // The grid anchors.fill: parent - GridView { - id: grid - visible: root.wallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: width / root.columns - cellHeight: cellWidth / root.previewCellAspectRatio - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded + 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 + } - 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; + 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 } } - onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } + GridView { + id: grid + visible: root.wallpapers.length > 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: ScrollBar { + policy: ScrollBar.AsNeeded } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - delegate: Item { - id: wallpaperItem - required property var modelData - required property int index - visible: modelData.length > 0 - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors { - fill: parent - margins: 8 + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } } - radius: Appearance.rounding.normal - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } - ColumnLayout { - id: wallpaperItemColumnLayout + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + model: ScriptModel { + values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) + } + onModelChanged: currentIndex = 0 + + delegate: Item { + id: wallpaperItem + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { anchors { fill: parent - margins: 6 + margins: 8 + } + radius: Appearance.rounding.normal + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } - spacing: 4 - Item { - id: wallpaperItemImageContainer - Layout.fillHeight: true - Layout.fillWidth: true - - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 } + spacing: 4 - Loader { - id: thumbnailImageLoader - anchors.fill: parent - active: wallpaperItem.visible - sourceComponent: Image { - id: thumbnailImage - source: { - if (wallpaperItem.modelData.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true - fillMode: Image.PreserveAspectCrop - clip: true - sourceSize.width: wallpaperItemColumnLayout.width - sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: wallpaperItem.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (wallpaperItem.modelData.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: wallpaperItemImageContainer.width - height: wallpaperItemImageContainer.height - radius: Appearance.rounding.small + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } } } } } - } - StyledText { - id: wallpaperItemName - Layout.fillWidth: true - Layout.leftMargin: 10 - Layout.rightMargin: 10 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - font.pixelSize: Appearance.font.pixelSize.smaller - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 - text: FileUtils.fileNameForPath(wallpaperItem.modelData) - } - } - } + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + 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) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData); + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(wallpaperItem.modelData); + } } } } - } - Label { - id: noWallpapersFoundLabel - visible: grid.model.values.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } - - StyledRectangularShadow { - target: extraOptionsBackground - } - - Rectangle { // Bottom toolbar - id: extraOptionsBackground - property real padding: 6 - anchors { - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - bottomMargin: 8 - } - color: Appearance.colors.colLayer2 - implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 - implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 - radius: Appearance.rounding.full - - RowLayout { - id: extraOptionsRowLayout + Item { + id: extraOptions anchors { - fill: parent - margins: extraOptionsBackground.padding + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2 + implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2 + + StyledRectangularShadow { + target: extraOptionsBackground } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - Wallpapers.openFallbackPicker(); - GlobalStates.wallpaperSelectorOpen = false; + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 + anchors { + fill: parent + margins: 8 } - contentItem: RowLayout { - MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledText { - text: Translation.tr("System") - } - } - StyledToolTip { - content: "Use the system file picker instead" - } - } + color: Appearance.colors.colLayer2 + implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 + implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 + radius: Appearance.rounding.full - TextField { - id: filterField - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: 200 - padding: 10 - placeholderText: Translation.tr("Search wallpapers...") - 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: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - wallpaperGrid.forceActiveFocus(); - } + RowLayout { + id: extraOptionsRowLayout + anchors { + fill: parent + margins: extraOptionsBackground.padding } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + Wallpapers.openFallbackPicker(); GlobalStates.wallpaperSelectorOpen = false; } - event.accepted = true; + contentItem: RowLayout { + MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger + } + StyledText { + text: Translation.tr("System") + } + } + StyledToolTip { + content: "Use the system file picker instead" + } } - } - } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - } - implicitWidth: height + TextField { + id: filterField + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: 200 + padding: 10 + placeholderText: Translation.tr("Search wallpapers...") + 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 + } - contentItem: MaterialSymbol { - text: "close" - iconSize: Appearance.font.pixelSize.larger - } + onTextChanged: { + root.filterQuery = text; + } - StyledToolTip { - content: "Cancel" + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + wallpaperGrid.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + wallpaperGrid.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + } + implicitWidth: height + + contentItem: MaterialSymbol { + text: "close" + iconSize: Appearance.font.pixelSize.larger + } + + StyledToolTip { + content: "Cancel" + } + } } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index eaa5fd22d..91c3c4b70 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -14,7 +14,7 @@ pragma ComponentBehavior: Bound Singleton { id: root - property string searchDir: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] @@ -42,7 +42,7 @@ Singleton { // Folder model FolderListModel { id: files - folder: Qt.resolvedUrl(root.searchDir) + folder: Qt.resolvedUrl(root.directory) nameFilters: root.extensions.map(ext => `*.${ext}`) showDirs: false showDotAndDotDot: false From 80af8666503aa16bccf836c956c9cfb92bfc10c5 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:11:41 +0700 Subject: [PATCH 26/68] wallpaper selector: directory validation, common file browser keybinds --- .../ii/modules/common/AddressBar.qml | 62 +++++++----- .../common/widgets/StyledTextInput.qml | 1 + .../WallpaperSelectorContent.qml | 95 +++++++++---------- .config/quickshell/ii/services/Wallpapers.qml | 25 ++++- 4 files changed, 106 insertions(+), 77 deletions(-) diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index b2c1f2bed..bc9b7080f 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -17,6 +17,11 @@ Rectangle { implicitHeight: mainLayout.implicitHeight + padding * 2 color: Appearance.colors.colLayer2 + function focusBreadcrumb() { + root.showBreadcrumb = false; + addressInput.forceActiveFocus(); + } + RowLayout { id: mainLayout anchors { @@ -38,43 +43,54 @@ Rectangle { Layout.fillWidth: true Layout.fillHeight: true - Loader { - active: !root.showBreadcrumb + Rectangle { + id: directoryEntry visible: !root.showBreadcrumb anchors.fill: parent - sourceComponent: Rectangle { - color: Appearance.colors.colLayer1 - radius: Appearance.rounding.full - implicitWidth: addressInput.implicitWidth - implicitHeight: addressInput.implicitHeight + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full + implicitWidth: addressInput.implicitWidth + implicitHeight: addressInput.implicitHeight - StyledTextInput { - id: addressInput + 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 + + onAccepted: { + root.navigateToDirectory(text); + root.showBreadcrumb = true; + } + + MouseArea { + // I-beam cursor 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 - } + 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) + onNavigateToDirectory: dir => { + root.navigateToDirectory(dir); } } } diff --git a/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml index 57d0c7262..ff98af721 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml @@ -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 diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 7ddf4033c..8f49f349d 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -20,11 +20,15 @@ Item { implicitWidth: columnLayout.implicitWidth property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" + property bool useDarkMode: Appearance.m3colors.darkmode 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; @@ -32,11 +36,7 @@ Item { grid.moveSelection(1); event.accepted = true; } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); - } + grid.moveSelection(-grid.columns); event.accepted = true; } else if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); @@ -50,6 +50,9 @@ Item { } filterField.forceActiveFocus(); event.accepted = true; + } else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) { + addressBar.focusBreadcrumb(); + event.accepted = true; } else { filterField.forceActiveFocus(); if (event.text.length > 0) { @@ -103,7 +106,7 @@ Item { Layout.fillHeight: false directory: Wallpapers.directory onNavigateToDirectory: path => { - Wallpapers.directory = path; + Wallpapers.setDirectory(path); } radius: wallpaperGridBackground.radius - Layout.margins } @@ -154,12 +157,13 @@ Item { } function activateCurrent() { - const path = model[currentIndex]; + print("ACTIVATE"); + const path = grid.model.values[currentIndex]; if (!path) return; GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; - Wallpapers.apply(path); + Wallpapers.apply(path, root.useDarkMode); } model: ScriptModel { @@ -284,7 +288,7 @@ Item { onClicked: { GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData); + Wallpapers.apply(wallpaperItem.modelData, root.useDarkMode); } } } @@ -326,22 +330,34 @@ Item { Layout.fillHeight: true Layout.topMargin: 2 Layout.bottomMargin: 2 + implicitWidth: height buttonRadius: Appearance.rounding.full onClicked: { Wallpapers.openFallbackPicker(); GlobalStates.wallpaperSelectorOpen = false; } - contentItem: RowLayout { - MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledText { - text: Translation.tr("System") - } + contentItem: MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger } StyledToolTip { - content: "Use the system file picker instead" + 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)") } } @@ -369,35 +385,20 @@ Item { } Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { + if (text.length !== 0) { + // No filtering, just navigate grid if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); - event.accepted = true; wallpaperGrid.forceActiveFocus(); + event.accepted = true; + } + if (event.key === Qt.Key_Up) { + grid.moveSelection(-grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; } } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } + event.accepted = false; } } @@ -409,15 +410,9 @@ Item { onClicked: { GlobalStates.wallpaperSelectorOpen = false; } - implicitWidth: height - contentItem: MaterialSymbol { - text: "close" - iconSize: Appearance.font.pixelSize.larger - } - - StyledToolTip { - content: "Cancel" + contentItem: StyledText { + text: "Cancel" } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 91c3c4b70..60c4e041b 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -30,15 +30,32 @@ Singleton { applyProc.exec([Directories.wallpaperSwitchScriptPath]) } - function apply(path) { + function apply(path, darkMode = Appearance.m3colors.darkmode) { if (!path || path.length === 0) return applyProc.exec([ - "bash", "-c", - `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + - `--image ${StringUtils.shellSingleQuoteEscape(path)}` + Directories.wallpaperSwitchScriptPath, + "--image", path, + "--mode", (darkMode ? "dark" : "light") ]) } + Process { + id: validateDirProc + property string nicePath: "" + function setDirectoryIfValid(path) { + validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "") + 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: files From 29a149b3403840bb9d8db17cc84ef8d117c20314 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:23:28 +0700 Subject: [PATCH 27/68] wallpaper selector: fix some key focus/fallthrough problems --- .config/quickshell/ii/modules/common/AddressBar.qml | 9 ++++++--- .../wallpaperSelector/WallpaperSelectorContent.qml | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index bc9b7080f..c25e1c0a4 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -67,9 +67,12 @@ Rectangle { padding: 10 text: root.directory - onAccepted: { - root.navigateToDirectory(text); - root.showBreadcrumb = true; + Keys.onPressed: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.navigateToDirectory(text); + root.showBreadcrumb = true; + event.accepted = true; + } } MouseArea { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 8f49f349d..668e57dca 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -53,11 +53,14 @@ Item { } else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) { addressBar.focusBreadcrumb(); event.accepted = true; - } else { + } 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; } @@ -368,7 +371,7 @@ Item { Layout.bottomMargin: 2 implicitWidth: 200 padding: 10 - placeholderText: Translation.tr("Search wallpapers...") + 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 From bdc0ade117da90dacdad49d76cdfc25793c68222 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:16:18 +0700 Subject: [PATCH 28/68] wallpaper selector: quick places --- .../ii/modules/common/AddressBar.qml | 4 + .../ii/modules/common/Directories.qml | 7 +- .../common/widgets/AddressBreadcrumb.qml | 5 +- .../WallpaperDirectoryItem.qml | 131 ++++ .../WallpaperSelectorContent.qml | 610 ++++++++---------- .config/quickshell/ii/services/Wallpapers.qml | 3 +- 6 files changed, 428 insertions(+), 332 deletions(-) create mode 100644 .config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index c25e1c0a4..a5328d446 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -108,6 +108,10 @@ Rectangle { iconSize: Appearance.font.pixelSize.larger color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2 } + + StyledToolTip { + content: Translation.tr("Edit directory") + } } } } diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index 2b235c691..49f0e46da 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -8,13 +8,16 @@ 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 genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] - readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[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 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") diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml index a2d3e0e39..d1d6b52df 100644 --- a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml +++ b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml @@ -27,7 +27,10 @@ ListView { required property var modelData required property int index buttonText: index === 0 ? "/" : modelData - toggled: index === directory.split("/").length - 1 + toggled: { + if (directory.trim() === "/") return index === 0; + return index === directory.split("/").length - 1 + } leftmost: index === 0 rightmost: index === breadcrumbDirectory.split("/").length - 1 diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml new file mode 100644 index 000000000..1e940ece2 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -0,0 +1,131 @@ +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 + required property string path + property bool isHovered: false + + property alias color: background.color + property alias radius: background.radius + property alias padding: background.anchors.margins + + signal activated() + + Rectangle { + id: background + anchors { + fill: parent + margins: 8 + } + radius: Appearance.rounding.normal + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 + } + spacing: 4 + + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true + + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } + + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: root.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (root.path.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(root.path); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false + + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } + } + } + } + } + + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + 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(root.path) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: root.activated() + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 668e57dca..2fc14a9ef 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -9,15 +9,11 @@ import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Hyprland Item { id: root property int columns: 4 property real previewCellAspectRatio: 4 / 3 - implicitHeight: columnLayout.implicitHeight - implicitWidth: columnLayout.implicitWidth property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" property bool useDarkMode: Appearance.m3colors.darkmode @@ -66,357 +62,315 @@ Item { } } - ColumnLayout { - id: columnLayout - anchors.fill: parent - spacing: -Appearance.sizes.elevationMargin + implicitHeight: mainLayout.implicitHeight + implicitWidth: mainLayout.implicitWidth - Item { // The grid - id: wallpaperGrid - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: wallpaperGridBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 - implicitHeight: wallpaperGridBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + 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 - 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 + Layout.fillHeight: true + Layout.margins: 4 + implicitWidth: quickDirColumnLayout.implicitWidth + implicitHeight: quickDirColumnLayout.implicitHeight + color: Appearance.colors.colLayer1 + radius: wallpaperGridBackground.radius - Layout.margins ColumnLayout { - // The grid + id: quickDirColumnLayout anchors.fill: parent - - AddressBar { - id: addressBar - Layout.margins: 4 - Layout.fillWidth: true - Layout.fillHeight: false - directory: Wallpapers.directory - onNavigateToDirectory: path => { - Wallpapers.setDirectory(path); + 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: "folder", 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: root.wallpapers.length > 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: ScrollBar { + policy: ScrollBar.AsNeeded + } + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + + function activateCurrent() { + print("ACTIVATE"); + const path = grid.model.values[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path, root.useDarkMode); + } + + model: ScriptModel { + values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) + } + onModelChanged: currentIndex = 0 + + delegate: WallpaperDirectoryItem { + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + path: modelData + color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + onActivated: { + Wallpapers.apply(path, root.useDarkMode); + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + } } - 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 - } + 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 } - GridView { - id: grid - visible: root.wallpapers.length > 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: ScrollBar { - policy: ScrollBar.AsNeeded - } - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - - function activateCurrent() { - print("ACTIVATE"); - const path = grid.model.values[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path, root.useDarkMode); - } - - model: ScriptModel { - values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) - } - onModelChanged: currentIndex = 0 - - delegate: Item { - id: wallpaperItem - required property var modelData - required property int index - visible: modelData.length > 0 - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors { - fill: parent - margins: 8 - } - radius: Appearance.rounding.normal - 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 { - id: wallpaperItemColumnLayout - anchors { - fill: parent - margins: 6 - } - spacing: 4 - - Item { - id: wallpaperItemImageContainer - Layout.fillHeight: true - Layout.fillWidth: true - - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small - } - - Loader { - id: thumbnailImageLoader - anchors.fill: parent - active: wallpaperItem.visible - sourceComponent: Image { - id: thumbnailImage - source: { - if (wallpaperItem.modelData.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false - - fillMode: Image.PreserveAspectCrop - clip: true - sourceSize.width: wallpaperItemColumnLayout.width - sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: wallpaperItemImageContainer.width - height: wallpaperItemImageContainer.height - radius: Appearance.rounding.small - } - } - } - } - } - - StyledText { - id: wallpaperItemName - Layout.fillWidth: true - Layout.leftMargin: 10 - Layout.rightMargin: 10 - - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - font.pixelSize: Appearance.font.pixelSize.smaller - 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) - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData, root.useDarkMode); - } - } - } - } - - Item { - id: extraOptions + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 anchors { - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter + fill: parent + margins: 8 } - implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2 - implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2 + color: Appearance.colors.colLayer2 + implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 + implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 + radius: Appearance.rounding.full - StyledRectangularShadow { - target: extraOptionsBackground - } - - Rectangle { // Bottom toolbar - id: extraOptionsBackground - property real padding: 6 + RowLayout { + id: extraOptionsRowLayout anchors { fill: parent - margins: 8 + margins: extraOptionsBackground.padding } - 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(); + 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 } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: height - buttonRadius: Appearance.rounding.full - onClicked: { - Wallpapers.openFallbackPicker(); - GlobalStates.wallpaperSelectorOpen = false; - } - contentItem: MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledToolTip { - content: Translation.tr("Use the system file picker instead") - } + onTextChanged: { + root.filterQuery = text; } - 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: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length !== 0) { - // No filtering, just navigate grid - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - wallpaperGrid.forceActiveFocus(); - event.accepted = true; - } - if (event.key === Qt.Key_Up) { - grid.moveSelection(-grid.columns); - wallpaperGrid.forceActiveFocus(); - event.accepted = true; - } + Keys.onPressed: event => { + if (text.length !== 0) { + // No filtering, just navigate grid + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; + } + if (event.key === Qt.Key_Up) { + grid.moveSelection(-grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; } - event.accepted = false; } + event.accepted = false; + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - } - - contentItem: StyledText { - text: "Cancel" - } + contentItem: StyledText { + text: "Cancel" } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 60c4e041b..d01e38034 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -44,6 +44,7 @@ Singleton { 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) => { @@ -61,7 +62,7 @@ Singleton { id: files folder: Qt.resolvedUrl(root.directory) nameFilters: root.extensions.map(ext => `*.${ext}`) - showDirs: false + showDirs: true showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time From 8bbf040100a02c927a556b486267b6906810d6b0 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 12:20:04 +0700 Subject: [PATCH 29/68] wallpaper selector: show folders --- .../ii/modules/common/Directories.qml | 1 + .../quickshell/ii/modules/common/Images.qml | 12 +++ .../common/{ => widgets}/AddressBar.qml | 0 .../modules/common/widgets/DirectoryIcon.qml | 42 ++++++++++ .../WallpaperDirectoryItem.qml | 41 ++++++--- .../WallpaperSelectorContent.qml | 84 +++++++++++++------ .config/quickshell/ii/services/Wallpapers.qml | 37 ++++++-- 7 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/Images.qml rename .config/quickshell/ii/modules/common/{ => widgets}/AddressBar.qml (100%) create mode 100644 .config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index 49f0e46da..60f587b59 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -16,6 +16,7 @@ Singleton { 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://" diff --git a/.config/quickshell/ii/modules/common/Images.qml b/.config/quickshell/ii/modules/common/Images.qml new file mode 100644 index 000000000..ac76f5118 --- /dev/null +++ b/.config/quickshell/ii/modules/common/Images.qml @@ -0,0 +1,12 @@ +pragma Singleton + +import Quickshell + +Singleton { + readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] + readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + + function isValidImageByName(name: string): bool { + return validImageExtensions.some(t => name.endsWith(`.${t}`)); + } +} diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml similarity index 100% rename from .config/quickshell/ii/modules/common/AddressBar.qml rename to .config/quickshell/ii/modules/common/widgets/AddressBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml new file mode 100644 index 000000000..b31ad021c --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -0,0 +1,42 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common + +// From https://github.com/caelestia-dots/shell with modifications. +// License: GPLv3 + +Image { + id: root + required property var fileModelData + asynchronous: true + fillMode: Image.PreserveAspectFit + + source: { + if (!fileModelData.fileIsDir) + return Quickshell.iconPath("application-x-zerosize"); + + const name = fileModelData.fileName; + const homeDir = Directories.home + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(name)) + return Quickshell.iconPath(`folder-${name.toLowerCase()}`); + + return Quickshell.iconPath("inode-directory"); + } + + onStatusChanged: { + if (status === Image.Error) + source = Quickshell.iconPath("error"); + } + + Process { + running: !fileModelData.fileIsDir + command: ["file", "--mime", "-b", fileModelData.filePath] + stdout: StdioCollector { + onStreamFinished: { + const mime = text.split(";")[0].replace("/", "-"); + root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing"); + } + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 1e940ece2..c1f0b83c9 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -12,14 +12,16 @@ import Quickshell.Io Item { id: root - required property string path + required property var fileModelData + property bool isDirectory: fileModelData.fileIsDir + property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) property bool isHovered: false property alias color: background.color property alias radius: background.radius property alias padding: background.anchors.margins - signal activated() + signal activated Rectangle { id: background @@ -45,21 +47,27 @@ Item { Layout.fillHeight: true Layout.fillWidth: true - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small + 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.visible + active: root.useThumbnail sourceComponent: Image { id: thumbnailImage source: { - if (root.path.length == 0) + if (fileModelData.filePath.length == 0) return; - const resolvedUrl = Qt.resolvedUrl(root.path); + const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath); const md5Hash = Qt.md5(resolvedUrl); const cacheSize = "normal"; const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; @@ -79,6 +87,8 @@ Item { Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } + onStatusChanged: if (status === Image.Error) + root.useThumbnail = false layer.enabled: true layer.effect: OpacityMask { @@ -90,6 +100,17 @@ Item { } } } + + Loader { + id: iconLoader + active: !root.useThumbnail + anchors.fill: parent + sourceComponent: DirectoryIcon { + fileModelData: root.fileModelData + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + } + } } StyledText { @@ -105,7 +126,7 @@ Item { Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } - text: FileUtils.fileNameForPath(root.path) + text: fileModelData.fileName } } } @@ -128,4 +149,4 @@ Item { } onClicked: root.activated() } -} \ No newline at end of file +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 2fc14a9ef..f6c19b5c1 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -14,8 +14,6 @@ Item { id: root property int columns: 4 property real previewCellAspectRatio: 4 / 3 - property var wallpapers: Wallpapers.wallpapers - property string filterQuery: "" property bool useDarkMode: Appearance.m3colors.darkmode Keys.onPressed: event => { @@ -102,7 +100,7 @@ Item { id: quickDirColumnLayout anchors.fill: parent spacing: 0 - + StyledText { Layout.margins: 12 font { @@ -118,14 +116,46 @@ Item { implicitWidth: 140 clip: true model: [ - { icon: "home", name: "Home", path: Directories.home }, - { icon: "folder", 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` }, + { + icon: "home", + name: "Home", + path: Directories.home + }, + { + icon: "folder", + 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 @@ -191,7 +221,7 @@ Item { GridView { id: grid - visible: root.wallpapers.length > 0 + visible: Wallpapers.folderModel.count > 0 readonly property int columns: root.columns readonly property int rows: Math.max(1, Math.ceil(count / columns)) @@ -217,36 +247,29 @@ Item { item.isHovered = false; } } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); } function activateCurrent() { - print("ACTIVATE"); - const path = grid.model.values[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; + const filePath = grid.model.get(currentIndex, "filePath") + Wallpapers.select(filePath, root.useDarkMode); filterField.text = ""; - Wallpapers.apply(path, root.useDarkMode); } - model: ScriptModel { - values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) - } + model: Wallpapers.folderModel onModelChanged: currentIndex = 0 delegate: WallpaperDirectoryItem { required property var modelData required property int index - visible: modelData.length > 0 + fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - path: modelData color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) onActivated: { - Wallpapers.apply(path, root.useDarkMode); - GlobalStates.wallpaperSelectorOpen = false; + Wallpapers.select(fileModelData.filePath, root.useDarkMode); + filterField.text = ""; } } @@ -339,7 +362,7 @@ Item { } onTextChanged: { - root.filterQuery = text; + Wallpapers.searchQuery = text; } Keys.onPressed: event => { @@ -389,4 +412,11 @@ Item { } } } + + Connections { + target: Wallpapers + function onChanged() { + GlobalStates.wallpaperSelectorOpen = false; + } + } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d01e38034..b98d3fe70 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,12 +15,15 @@ Singleton { id: root property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + property alias folderModel: folderModel // Expose for direct binding when needed + property string searchQuery: "" readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] - property alias filesModel: files // Expose for direct binding when needed property list wallpapers: [] // List of absolute file paths (without file://) + signal changed() + // Executions Process { id: applyProc @@ -37,6 +40,29 @@ Singleton { "--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); } Process { @@ -59,9 +85,10 @@ Singleton { // Folder model FolderListModel { - id: files + id: folderModel folder: Qt.resolvedUrl(root.directory) - nameFilters: root.extensions.map(ext => `*.${ext}`) + caseSensitive: false + nameFilters: root.extensions.map(ext => `*${searchQuery.split(" ").filter(s => s.length > 0).map(s => `*${s}*`)}*.${ext}`) showDirs: true showDotAndDotDot: false showOnlyReadable: true @@ -69,8 +96,8 @@ Singleton { sortReversed: false onCountChanged: { root.wallpapers = [] - for (let i = 0; i < files.count; i++) { - const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) + 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) } } From 8277a2d942f383feed7f019b48d352b6c6d4015f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:25:39 +0700 Subject: [PATCH 30/68] wallpaper selector: fix dark mode --- .../modules/common/widgets/DirectoryIcon.qml | 5 +- .../WallpaperDirectoryItem.qml | 4 +- .../WallpaperSelectorContent.qml | 55 ++++--------------- .config/quickshell/ii/services/Wallpapers.qml | 9 ++- 4 files changed, 21 insertions(+), 52 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml index b31ad021c..acdb65898 100644 --- a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -16,10 +16,9 @@ Image { if (!fileModelData.fileIsDir) return Quickshell.iconPath("application-x-zerosize"); - const name = fileModelData.fileName; const homeDir = Directories.home - if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(name)) - return Quickshell.iconPath(`folder-${name.toLowerCase()}`); + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(fileModelData.filePath)) + return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`); return Quickshell.iconPath("inode-directory"); } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index c1f0b83c9..66f3f9e24 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -17,7 +17,8 @@ Item { property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) property bool isHovered: false - property alias color: background.color + property alias colBackground: background.color + property alias colText: wallpaperItemName.color property alias radius: background.radius property alias padding: background.anchors.margins @@ -122,7 +123,6 @@ Item { horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.smaller - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index f6c19b5c1..8a985bfb0 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -116,46 +116,14 @@ Item { implicitWidth: 140 clip: true model: [ - { - icon: "home", - name: "Home", - path: Directories.home - }, - { - icon: "folder", - 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` - }, + { 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 @@ -266,7 +234,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + colBackground: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 onActivated: { Wallpapers.select(fileModelData.filePath, root.useDarkMode); @@ -314,7 +283,7 @@ Item { implicitWidth: height buttonRadius: Appearance.rounding.full onClicked: { - Wallpapers.openFallbackPicker(); + Wallpapers.openFallbackPicker(root.useDarkMode); GlobalStates.wallpaperSelectorOpen = false; } contentItem: MaterialSymbol { @@ -370,12 +339,10 @@ Item { // No filtering, just navigate grid if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); - wallpaperGrid.forceActiveFocus(); event.accepted = true; } if (event.key === Qt.Key_Up) { grid.moveSelection(-grid.columns); - wallpaperGrid.forceActiveFocus(); event.accepted = true; } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index b98d3fe70..d1c211518 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -29,8 +29,11 @@ Singleton { id: applyProc } - function openFallbackPicker() { - applyProc.exec([Directories.wallpaperSwitchScriptPath]) + function openFallbackPicker(darkMode = Appearance.m3colors.darkmode) { + applyProc.exec([ + Directories.wallpaperSwitchScriptPath, + "--mode", (darkMode ? "dark" : "light") + ]) } function apply(path, darkMode = Appearance.m3colors.darkmode) { @@ -62,7 +65,7 @@ Singleton { } function select(filePath, darkMode = Appearance.m3colors.darkmode) { - selectProc.select(filePath); + selectProc.select(filePath, darkMode); } Process { From ef4ae4480f66be85fb11a803a0c8ee5d7431af19 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:12:37 +0700 Subject: [PATCH 31/68] wallpaper selector: not reinvent MouseArea containsMouse --- .../WallpaperDirectoryItem.qml | 25 +++---------------- .../WallpaperSelectorContent.qml | 16 ++++++------ 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 66f3f9e24..382880df4 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -10,12 +10,11 @@ import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Io -Item { +MouseArea { id: root required property var fileModelData property bool isDirectory: fileModelData.fileIsDir property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) - property bool isHovered: false property alias colBackground: background.color property alias colText: wallpaperItemName.color @@ -24,6 +23,9 @@ Item { signal activated + hoverEnabled: true + onClicked: root.activated() + Rectangle { id: background anchors { @@ -130,23 +132,4 @@ Item { } } } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: root.activated() - } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 8a985bfb0..bc6d3408a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -209,12 +209,6 @@ Item { } function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); } @@ -234,11 +228,15 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + + onEntered: { + grid.currentIndex = index; + } + onActivated: { Wallpapers.select(fileModelData.filePath, root.useDarkMode); - filterField.text = ""; } } From f3ab3573c3e482b87f59af300990d2f446d42d00 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:39:50 +0700 Subject: [PATCH 32/68] wallpaper selector: style the scrollbar --- .../common/widgets/StyledScrollBar.qml | 26 +++++++++++++++++++ .../WallpaperSelectorContent.qml | 4 +-- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml new file mode 100644 index 000000000..ab357e09b --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml @@ -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 + } + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index bc6d3408a..f97909bd3 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -204,9 +204,7 @@ Item { boundsBehavior: Flickable.StopAtBounds bottomMargin: extraOptions.implicitHeight - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } + ScrollBar.vertical: StyledScrollBar {} function moveSelection(delta) { currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); From b2d14ca101762d7b6c911fb3d8a2d4cd12f6930d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:59:41 +0700 Subject: [PATCH 33/68] wallpaper selector: thumbnail generation, fix xdg dir folder icons --- .../ii/modules/common/ThumbnailImage.qml | 67 +++++++++++++++++++ .../modules/common/widgets/DirectoryIcon.qml | 4 +- .../WallpaperDirectoryItem.qml | 23 +------ .../WallpaperSelectorContent.qml | 4 +- 4 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/ThumbnailImage.qml diff --git a/.config/quickshell/ii/modules/common/ThumbnailImage.qml b/.config/quickshell/ii/modules/common/ThumbnailImage.qml new file mode 100644 index 000000000..a1a4c204f --- /dev/null +++ b/.config/quickshell/ii/modules/common/ThumbnailImage.qml @@ -0,0 +1,67 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.functions + +/** + * Thumbnail image. + * See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html + */ +Image { + id: root + + required property string sourcePath + readonly property var thumbnailSizes: ({ + "normal": 128, + "large": 256, + "x-large": 512, + "xx-large": 1024 + }) + property string thumbnailSizeName: { // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html + const sizeNames = Object.keys(thumbnailSizes); + for(let i = 0; i < sizeNames.length; i++) { + const sizeName = sizeNames[i]; + const maxSize = thumbnailSizes[sizeName]; + if (root.sourceSize.width <= maxSize && root.sourceSize.height <= maxSize) return sizeName; + } + return "xx-large"; + } + 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: { + thumbnailGeneration.running = false + thumbnailGeneration.running = true + } + Process { + id: thumbnailGeneration + command: { + const maxSize = root.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 + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml index acdb65898..9df2ee2fe 100644 --- a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -2,6 +2,7 @@ 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 @@ -16,8 +17,7 @@ Image { if (!fileModelData.fileIsDir) return Quickshell.iconPath("application-x-zerosize"); - const homeDir = Directories.home - if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(fileModelData.filePath)) + 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"); diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 382880df4..2616219c8 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -65,34 +65,15 @@ MouseArea { id: thumbnailImageLoader anchors.fill: parent active: root.useThumbnail - sourceComponent: Image { + sourceComponent: ThumbnailImage { id: thumbnailImage - source: { - if (fileModelData.filePath.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false + sourcePath: fileModelData.filePath fillMode: Image.PreserveAspectCrop clip: true sourceSize.width: wallpaperItemColumnLayout.width sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - onStatusChanged: if (status === Image.Error) - root.useThumbnail = false - layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index f97909bd3..11526ed3b 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -226,8 +226,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + colBackground: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colPrimary : (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnPrimary : (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 onEntered: { grid.currentIndex = index; From eb2c9f2fe12e9bcf23df4ca3ccd83c5fd47db450 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:58:29 +0700 Subject: [PATCH 34/68] wallpaper selector: adjust size and item colors, fix address bar inconsistency --- .config/quickshell/ii/modules/common/Appearance.qml | 4 ++-- .config/quickshell/ii/modules/common/widgets/AddressBar.qml | 5 ++++- .../modules/wallpaperSelector/WallpaperSelectorContent.qml | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index e1f0853a5..ddcbb3ca9 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -343,8 +343,8 @@ Singleton { property real baseVerticalBarWidth: 46 property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ? (baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth - property real wallpaperSelectorWidth: 1000 - property real wallpaperSelectorHeight: 580 + property real wallpaperSelectorWidth: 1200 + property real wallpaperSelectorHeight: 690 } syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light" diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBar.qml b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml index a5328d446..c965a536d 100644 --- a/.config/quickshell/ii/modules/common/widgets/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml @@ -8,7 +8,10 @@ import qs.modules.common.functions Rectangle { id: root required property var directory - property bool showBreadcrumb: true // TODO: make this work + property bool showBreadcrumb: true + onShowBreadcrumbChanged: { + addressInput.text = root.directory; + } signal navigateToDirectory(string path) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 11526ed3b..70d038ae1 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -226,8 +226,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colPrimary : (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnPrimary : (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + 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; From 20d95611438b4baed7aeb84022c38f7e6fc3d365 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:52:23 +0700 Subject: [PATCH 35/68] wifi menu --- .../modules/common/widgets/DialogButton.qml | 18 +- .../common/widgets/MaterialTextArea.qml | 52 +++++ .../common/widgets/MaterialTextField.qml | 35 +-- .../widgets/PointingHandInteraction.qml | 2 +- .../modules/common/widgets/StyledListView.qml | 84 +++---- .../modules/common/widgets/WindowDialog.qml | 83 +++++++ .../common/widgets/WindowDialogButtonRow.qml | 15 ++ .../common/widgets/WindowDialogSeparator.qml | 16 ++ .../common/widgets/WindowDialogTitle.qml | 13 ++ .../ii/modules/settings/ServicesConfig.qml | 12 +- .../ii/modules/sidebarRight/SidebarRight.qml | 122 +--------- .../sidebarRight/SidebarRightContent.qml | 220 ++++++++++++++++++ .../wifiNetworks/WifiNetworkItem.qml | 111 +++++++++ .config/quickshell/ii/services/Network.qml | 173 +++++++++++++- .../ii/services/network/WifiAccessPoint.qml | 14 ++ 15 files changed, 768 insertions(+), 202 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/WindowDialog.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml create mode 100644 .config/quickshell/ii/services/network/WifiAccessPoint.qml diff --git a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml index 972c29b29..ba00d9b3c 100644 --- a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml @@ -1,29 +1,35 @@ import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets import QtQuick /** * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview */ RippleButton { - id: button + id: root property string buttonText - implicitHeight: 30 - implicitWidth: buttonTextWidget.implicitWidth + 15 * 2 + padding: 14 + implicitHeight: 36 + implicitWidth: buttonTextWidget.implicitWidth + padding * 2 buttonRadius: Appearance?.rounding.full ?? 9999 property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F" property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96" + colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) + colBackgroundHover: Appearance.colors.colLayer3Hover + colRipple: Appearance.colors.colLayer3Active contentItem: StyledText { id: buttonTextWidget anchors.fill: parent - anchors.leftMargin: 15 - anchors.rightMargin: 15 + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding text: buttonText horizontalAlignment: Text.AlignHCenter font.pixelSize: Appearance?.font.pixelSize.small ?? 12 - color: button.enabled ? button.colEnabled : button.colDisabled + color: root.enabled ? root.colEnabled : root.colDisabled Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) diff --git a/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml b/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml new file mode 100644 index 000000000..241cc90f7 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml @@ -0,0 +1,52 @@ +import qs.modules.common +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls + +/** + * Material 3 styled TextArea (filled style) + * https://m3.material.io/components/text-fields/overview + * Note: We don't use NativeRendering because it makes the small placeholder text look weird + */ +TextArea { + id: root + Material.theme: Material.System + Material.accent: Appearance.m3colors.m3primary + Material.primary: Appearance.m3colors.m3primary + Material.background: Appearance.m3colors.m3surface + Material.foreground: Appearance.m3colors.m3onSurface + Material.containerStyle: Material.Filled + renderType: Text.QtRendering + + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + placeholderTextColor: Appearance.m3colors.m3outline + + background: Rectangle { + implicitHeight: 56 + color: Appearance.m3colors.m3surface + topLeftRadius: 4 + topRightRadius: 4 + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 1 + color: root.focus ? Appearance.m3colors.m3primary : + root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + } + + font { + family: Appearance?.font.family.main ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.small ?? 15 + hintingPreference: Font.PreferFullHinting + } + wrapMode: TextEdit.Wrap +} diff --git a/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml index 241cc90f7..f160a77d7 100644 --- a/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml +++ b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml @@ -4,44 +4,24 @@ import QtQuick.Controls.Material import QtQuick.Controls /** - * Material 3 styled TextArea (filled style) + * Material 3 styled TextField (filled style) * https://m3.material.io/components/text-fields/overview * Note: We don't use NativeRendering because it makes the small placeholder text look weird */ -TextArea { +TextField { id: root Material.theme: Material.System Material.accent: Appearance.m3colors.m3primary Material.primary: Appearance.m3colors.m3primary Material.background: Appearance.m3colors.m3surface Material.foreground: Appearance.m3colors.m3onSurface - Material.containerStyle: Material.Filled + Material.containerStyle: Material.Outlined renderType: Text.QtRendering selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer placeholderTextColor: Appearance.m3colors.m3outline - - background: Rectangle { - implicitHeight: 56 - color: Appearance.m3colors.m3surface - topLeftRadius: 4 - topRightRadius: 4 - Rectangle { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - height: 1 - color: root.focus ? Appearance.m3colors.m3primary : - root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant - - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - } - } + clip: true font { family: Appearance?.font.family.main ?? "sans-serif" @@ -49,4 +29,11 @@ TextArea { hintingPreference: Font.PreferFullHinting } wrapMode: TextEdit.Wrap + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + cursorShape: Qt.IBeamCursor + } } diff --git a/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml b/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml index cf8b065f7..a626bb958 100644 --- a/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml +++ b/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml @@ -3,5 +3,5 @@ import QtQuick MouseArea { anchors.fill: parent onPressed: (mouse) => mouse.accepted = false - cursorShape: Qt.PointingHandCursor + cursorShape: Qt.PointingHandCursor } \ No newline at end of file diff --git a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml index aebf35d77..f005e9f4d 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -14,6 +14,8 @@ ListView { property int dragIndex: -1 property real dragDistance: 0 property bool popin: true + property bool animateAppearance: true + property bool animateMovement: false // Accumulated scroll destination so wheel deltas stack while animating property real scrollTargetY: 0 @@ -66,17 +68,17 @@ ListView { } add: Transition { - animations: [ + animations: animateAppearance ? [ Appearance?.animation.elementMove.numberAnimation.createObject(this, { properties: popin ? "opacity,scale" : "opacity", from: 0, to: 1, }), - ] + ] : [] } addDisplaced: Transition { - animations: [ + animations: animateAppearance ? [ Appearance?.animation.elementMove.numberAnimation.createObject(this, { property: "y", }), @@ -84,46 +86,46 @@ ListView { properties: popin ? "opacity,scale" : "opacity", to: 1, }), - ] + ] : [] } - // displaced: Transition { - // animations: [ - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // property: "y", - // }), - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // properties: "opacity,scale", - // to: 1, - // }), - // ] - // } + displaced: Transition { + animations: root.animateMovement ? [ + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + property: "y", + }), + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + properties: "opacity,scale", + to: 1, + }), + ] : [] + } - // move: Transition { - // animations: [ - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // property: "y", - // }), - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // properties: "opacity,scale", - // to: 1, - // }), - // ] - // } - // moveDisplaced: Transition { - // animations: [ - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // property: "y", - // }), - // Appearance?.animation.elementMove.numberAnimation.createObject(this, { - // properties: "opacity,scale", - // to: 1, - // }), - // ] - // } + move: Transition { + animations: root.animateMovement ? [ + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + property: "y", + }), + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + properties: "opacity,scale", + to: 1, + }), + ] : [] + } + moveDisplaced: Transition { + animations: root.animateMovement ? [ + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + property: "y", + }), + Appearance?.animation.elementMove.numberAnimation.createObject(this, { + properties: "opacity,scale", + to: 1, + }), + ] : [] + } remove: Transition { - animations: [ + animations: animateAppearance ? [ Appearance?.animation.elementMove.numberAnimation.createObject(this, { property: "x", to: root.width + root.removeOvershoot, @@ -132,12 +134,12 @@ ListView { property: "opacity", to: 0, }) - ] + ] : [] } // This is movement when something is removed, not removing animation! removeDisplaced: Transition { - animations: [ + animations: animateAppearance ? [ Appearance?.animation.elementMove.numberAnimation.createObject(this, { property: "y", }), @@ -145,6 +147,6 @@ ListView { properties: "opacity,scale", to: 1, }), - ] + ] : [] } } diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml new file mode 100644 index 000000000..084f0bbed --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml @@ -0,0 +1,83 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets + +Rectangle { + id: root + + property bool show: false + default property alias data: contentColumn.data + property real backgroundHeight: 600 + property real backgroundAnimationMovementDistance: 60 + signal dismiss() + + color: root.show ? Appearance.colors.colScrim : ColorUtils.transparentize(Appearance.colors.colScrim) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + visible: dialogBackground.implicitHeight > 0 + + onShowChanged: { + dialogBackgroundHeightAnimation.easing.bezierCurve = (show ? Appearance.animationCurves.emphasizedDecel : Appearance.animationCurves.emphasizedAccel) + dialogBackground.implicitHeight = show ? backgroundHeight : 0 + } + + radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 + + MouseArea { // Clicking outside the dialog should dismiss + anchors.fill: parent + acceptedButtons: Qt.AllButtons + hoverEnabled: true + onPressed: root.dismiss() + } + + Rectangle { + id: dialogBackground + anchors.horizontalCenter: parent.horizontalCenter + radius: Appearance.rounding.large + color: Appearance.colors.colLayer3 + + property real targetY: root.height / 2 - root.backgroundHeight / 2 + y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance) + implicitWidth: 350 + implicitHeight: 0 + Behavior on implicitHeight { + NumberAnimation { + id: dialogBackgroundHeightAnimation + duration: Appearance.animation.elementMoveFast.duration + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.animationCurves.emphasizedDecel + } + } + Behavior on y { + NumberAnimation { + duration: dialogBackgroundHeightAnimation.duration + easing.type: dialogBackgroundHeightAnimation.easing.type + easing.bezierCurve: dialogBackgroundHeightAnimation.easing.bezierCurve + } + } + + MouseArea { // So clicking inside the dialog won't dismiss + anchors.fill: parent + acceptedButtons: Qt.AllButtons + hoverEnabled: true + } + + ColumnLayout { + id: contentColumn + anchors { + fill: parent + margins: dialogBackground.radius + } + spacing: 16 + opacity: root.show ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + } + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml new file mode 100644 index 000000000..0672e96a5 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets + +RowLayout { + id: root + spacing: 4 + + // These shouldn't be needed but it would be a terrible waste of space to follow the spec + Layout.margins: -8 + Layout.topMargin: 0 +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml new file mode 100644 index 000000000..52707e51e --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets + +Rectangle { + implicitHeight: 1 + color: Appearance.colors.colOutline + Layout.fillWidth: true + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + Layout.topMargin: -8 + Layout.bottomMargin: -8 +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml new file mode 100644 index 000000000..a450030fa --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml @@ -0,0 +1,13 @@ +import QtQuick +import Quickshell +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets + +StyledText { + text: "Dialog Title" + font { + pixelSize: Appearance.font.pixelSize.title + family: Appearance.font.family.title + } +} diff --git a/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/.config/quickshell/ii/modules/settings/ServicesConfig.qml index 712042d65..1b1dc429c 100644 --- a/.config/quickshell/ii/modules/settings/ServicesConfig.qml +++ b/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -49,7 +49,7 @@ ContentPage { } ContentSection { title: Translation.tr("AI") - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("System prompt") text: Config.options.ai.systemPrompt @@ -115,7 +115,7 @@ ContentPage { ContentSection { title: Translation.tr("Networking") - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("User agent (for services that require it)") text: Config.options.networking.userAgent @@ -159,7 +159,7 @@ ContentPage { ConfigRow { uniform: true - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("Action") text: Config.options.search.prefix.action @@ -168,7 +168,7 @@ ContentPage { Config.options.search.prefix.action = text; } } - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("Clipboard") text: Config.options.search.prefix.clipboard @@ -177,7 +177,7 @@ ContentPage { Config.options.search.prefix.clipboard = text; } } - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("Emojis") text: Config.options.search.prefix.emojis @@ -190,7 +190,7 @@ ContentPage { } ContentSubsection { title: Translation.tr("Web search") - MaterialTextField { + MaterialTextArea { Layout.fillWidth: true placeholderText: Translation.tr("Base URL") text: Config.options.search.engineBaseUrl diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml index 3d2c406ca..4147c3abe 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml @@ -3,7 +3,6 @@ import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions -import "./quickToggles/" import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -16,8 +15,6 @@ import Quickshell.Hyprland Scope { id: root property int sidebarWidth: Appearance.sizes.sidebarWidth - property int sidebarPadding: 12 - property string settingsQmlPath: Quickshell.shellPath("settings.qml") PanelWindow { id: sidebarRoot @@ -67,124 +64,7 @@ Scope { } } - sourceComponent: Item { - implicitHeight: sidebarRightBackground.implicitHeight - implicitWidth: sidebarRightBackground.implicitWidth - - StyledRectangularShadow { - target: sidebarRightBackground - } - Rectangle { - id: sidebarRightBackground - - anchors.fill: parent - implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2 - implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2 - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: sidebarPadding - spacing: sidebarPadding - - RowLayout { - Layout.fillHeight: false - spacing: 10 - Layout.margins: 10 - Layout.topMargin: 5 - Layout.bottomMargin: 0 - - CustomIcon { - id: distroIcon - width: 25 - height: 25 - source: SystemInfo.distroIcon - colorize: true - color: Appearance.colors.colOnLayer0 - } - - StyledText { - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colOnLayer0 - text: Translation.tr("Up %1").arg(DateTime.uptime) - textFormat: Text.MarkdownText - } - - Item { - Layout.fillWidth: true - } - - ButtonGroup { - QuickToggleButton { - toggled: false - buttonIcon: "restart_alt" - onClicked: { - Hyprland.dispatch("reload") - Quickshell.reload(true) - } - StyledToolTip { - content: Translation.tr("Reload Hyprland & Quickshell") - } - } - QuickToggleButton { - toggled: false - buttonIcon: "settings" - onClicked: { - GlobalStates.sidebarRightOpen = false - Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]) - } - StyledToolTip { - content: Translation.tr("Settings") - } - } - QuickToggleButton { - toggled: false - buttonIcon: "power_settings_new" - onClicked: { - GlobalStates.sessionOpen = true - } - StyledToolTip { - content: Translation.tr("Session") - } - } - } - } - - ButtonGroup { - Layout.alignment: Qt.AlignHCenter - spacing: 5 - padding: 5 - color: Appearance.colors.colLayer1 - - NetworkToggle {} - BluetoothToggle {} - NightLight {} - GameMode {} - IdleInhibitor {} - EasyEffectsToggle {} - CloudflareWarp {} - } - - // Center widget group - CenterWidgetGroup { - focus: sidebarRoot.visible - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.fillWidth: true - } - - BottomWidgetGroup { - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: false - Layout.fillWidth: true - Layout.preferredHeight: implicitHeight - } - } - } - } + sourceComponent: SidebarRightContent {} } diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml new file mode 100644 index 000000000..88440d11a --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -0,0 +1,220 @@ +import qs +import qs.services +import qs.services.network +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import "./quickToggles/" +import "./wifiNetworks/" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell.Io +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { + id: root + property int sidebarWidth: Appearance.sizes.sidebarWidth + property int sidebarPadding: 12 + property string settingsQmlPath: Quickshell.shellPath("settings.qml") + property bool showDialog: false + property bool dialogIsWifi: true + + Connections { + target: GlobalStates + function onSidebarRightOpenChanged() { + if (!GlobalStates.sidebarRightOpen) { + root.showDialog = false + } + } + } + + implicitHeight: sidebarRightBackground.implicitHeight + implicitWidth: sidebarRightBackground.implicitWidth + + StyledRectangularShadow { + target: sidebarRightBackground + } + Rectangle { + id: sidebarRightBackground + + anchors.fill: parent + implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2 + implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2 + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 + + ColumnLayout { + anchors.fill: parent + anchors.margins: sidebarPadding + spacing: sidebarPadding + + RowLayout { + Layout.fillHeight: false + spacing: 10 + Layout.margins: 10 + Layout.topMargin: 5 + Layout.bottomMargin: 0 + + CustomIcon { + id: distroIcon + width: 25 + height: 25 + source: SystemInfo.distroIcon + colorize: true + color: Appearance.colors.colOnLayer0 + } + + StyledText { + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer0 + text: Translation.tr("Up %1").arg(DateTime.uptime) + textFormat: Text.MarkdownText + } + + Item { + Layout.fillWidth: true + } + + ButtonGroup { + QuickToggleButton { + toggled: false + buttonIcon: "restart_alt" + onClicked: { + Hyprland.dispatch("reload") + Quickshell.reload(true) + } + StyledToolTip { + content: Translation.tr("Reload Hyprland & Quickshell") + } + } + QuickToggleButton { + toggled: false + buttonIcon: "settings" + onClicked: { + GlobalStates.sidebarRightOpen = false + Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]) + } + StyledToolTip { + content: Translation.tr("Settings") + } + } + QuickToggleButton { + toggled: false + buttonIcon: "power_settings_new" + onClicked: { + GlobalStates.sessionOpen = true + } + StyledToolTip { + content: Translation.tr("Session") + } + } + } + } + + ButtonGroup { + Layout.alignment: Qt.AlignHCenter + spacing: 5 + padding: 5 + color: Appearance.colors.colLayer1 + + NetworkToggle { + altAction: () => { + Network.enableWifi() + Network.rescanWifi() + root.dialogIsWifi = true + root.showDialog = true + } + } + BluetoothToggle {} + NightLight {} + GameMode {} + IdleInhibitor {} + EasyEffectsToggle {} + CloudflareWarp {} + } + + CenterWidgetGroup { + focus: sidebarRoot.visible + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.fillWidth: true + } + + BottomWidgetGroup { + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: false + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + } + } + } + + WindowDialog { + show: root.showDialog + onDismiss: root.showDialog = false + anchors { + fill: parent + } + + WindowDialogTitle { + text: Translation.tr("Connect to Wi-Fi") + } + WindowDialogSeparator { + // TODO: add indeterminate progress bar when scanning + } + StyledListView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -15 + Layout.bottomMargin: -16 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + + clip: true + spacing: 0 + animateAppearance: false + + model: ScriptModel { + values: [...Network.wifiNetworks].sort((a, b) => { + if (a.active && !b.active) return -1; + if (!a.active && b.active) return 1; + return b.strength - a.strength; + }) + } + // model: Network.wifiNetworks + delegate: WifiNetworkItem { + required property WifiAccessPoint modelData + wifiNetwork: modelData + anchors { + left: parent?.left + right: parent?.right + } + } + } + WindowDialogSeparator {} + WindowDialogButtonRow { + DialogButton { + buttonText: Translation.tr("Details") + onClicked: { + Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]) + GlobalStates.sidebarRightOpen = false + } + } + + Item { + Layout.fillWidth: true + } + + DialogButton { + buttonText: Translation.tr("Done") + onClicked: root.showDialog = false + } + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml new file mode 100644 index 000000000..907ec9402 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -0,0 +1,111 @@ +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.services +import qs.services.network +import QtQuick +import QtQuick.Layouts +import Quickshell + +RippleButton { + id: root + required property WifiAccessPoint wifiNetwork + + horizontalPadding: Appearance.rounding.large + verticalPadding: 12 + implicitWidth: mainLayout.implicitWidth + horizontalPadding * 2 + implicitHeight: mainLayout.implicitHeight + verticalPadding * 2 + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + clip: true + + buttonRadius: 0 + colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) + colBackgroundHover: wifiNetwork?.askingPassword ? colBackground : Appearance.colors.colLayer3Hover + colRipple: Appearance.colors.colLayer3Active + + onClicked: { + Network.connectToWifiNetwork(wifiNetwork) + } + + contentItem: ColumnLayout { + id: mainLayout + anchors { + fill: parent + topMargin: root.verticalPadding + bottomMargin: root.verticalPadding + leftMargin: root.horizontalPadding + rightMargin: root.horizontalPadding + } + spacing: 0 + + RowLayout { + spacing: 10 + MaterialSymbol { + iconSize: Appearance.font.pixelSize.larger + text: root.wifiNetwork?.strength > 80 ? "signal_wifi_4_bar" : + root.wifiNetwork?.strength > 60 ? "network_wifi_3_bar" : + root.wifiNetwork?.strength > 40 ? "network_wifi_2_bar" : + root.wifiNetwork?.strength > 20 ? "network_wifi_1_bar" : + "signal_wifi_0_bar" + color: Appearance.colors.colOnSurfaceVariant + } + StyledText { + Layout.fillWidth: true + text: root.wifiNetwork?.ssid + color: Appearance.colors.colOnSurfaceVariant + } + MaterialSymbol { + visible: root.wifiNetwork?.isSecure || root.wifiNetwork?.active + text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock" + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnSurfaceVariant + } + } + + ColumnLayout { + id: passwordPrompt + visible: root.wifiNetwork?.askingPassword + Layout.topMargin: 12 + + MaterialTextField { + id: passwordField + Layout.fillWidth: true + placeholderText: Translation.tr("Password") + + // Password + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onAccepted: { + Network.changePassword(root.wifiNetwork, passwordField.text) + } + } + + RowLayout { + Layout.fillWidth: true + + Item { + Layout.fillWidth: true + } + + DialogButton { + buttonText: Translation.tr("Cancel") + onClicked: { + root.wifiNetwork.askingPassword = false + } + } + + DialogButton { + buttonText: Translation.tr("Connect") + onClicked: { + Network.changePassword(root.wifiNetwork, passwordField.text) + } + } + } + + } + } +} diff --git a/.config/quickshell/ii/services/Network.qml b/.config/quickshell/ii/services/Network.qml index d318aedbd..bb6e36f9c 100644 --- a/.config/quickshell/ii/services/Network.qml +++ b/.config/quickshell/ii/services/Network.qml @@ -1,12 +1,15 @@ pragma Singleton pragma ComponentBehavior: Bound +// Took many bits from https://github.com/caelestia-dots/shell (GPLv3) + import Quickshell import Quickshell.Io import QtQuick +import "./network" /** - * Simple polled network state service. + * Network service with nmcli. */ Singleton { id: root @@ -15,6 +18,12 @@ Singleton { property bool ethernet: false property bool wifiEnabled: false + property bool wifiScanning: false + property bool wifiConnecting: connectProc.running + property WifiAccessPoint wifiConnectTarget + readonly property list wifiNetworks: [] + readonly property WifiAccessPoint active: wifiNetworks.find(n => n.active) ?? null + property string networkName: "" property int networkStrength property string materialSymbol: ethernet ? "lan" : @@ -27,15 +36,99 @@ Singleton { ) : "signal_wifi_off" // Control - function toggleWifi(): void { - const cmd = wifiEnabled ? "off" : "on"; + function enableWifi(enabled = true): void { + const cmd = enabled ? "on" : "off"; enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); } + function toggleWifi(): void { + enableWifi(!wifiEnabled); + } + + function rescanWifi(): void { + wifiScanning = true; + rescanProcess.running = true; + } + + function connectToWifiNetwork(accessPoint: WifiAccessPoint): void { + accessPoint.askingPassword = false; + root.wifiConnectTarget = accessPoint; + // We use this instead of `nmcli connection up SSID` because this also creates a connection profile + connectProc.exec(["nmcli", "dev", "wifi", "connect", accessPoint.ssid]) + + } + + function disconnectWifiNetwork(): void { + if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]); + } + + function changePassword(network: WifiAccessPoint, password: string, username = ""): void { + // TODO: enterprise wifi with username + network.askingPassword = false; + changePasswordProc.exec({ + "environment": { + "PASSWORD": password + }, + "command": ["bash", "-c", `nmcli connection modify ${network.ssid} wifi-sec.psk "$PASSWORD"`] + }) + } + Process { id: enableWifiProc } + Process { + id: connectProc + environment: ({ + LANG: "C", + LC_ALL: "C" + }) + stdout: SplitParser { + onRead: line => { + // print(line) + getNetworks.running = true + } + } + stderr: SplitParser { + onRead: line => { + // print("err:", line) + if (line.includes("Secrets were required")) { + root.wifiConnectTarget.askingPassword = true + } + } + } + onExited: (exitCode, exitStatus) => { + root.wifiConnectTarget.askingPassword = (exitCode !== 0) + root.wifiConnectTarget = null + } + } + + Process { + id: disconnectProc + stdout: SplitParser { + onRead: getNetworks.running = true + } + } + + Process { + id: changePasswordProc + onExited: { // Re-attempt connection after changing password + connectProc.running = false + connectProc.running = true + } + } + + Process { + id: rescanProcess + command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + stdout: SplitParser { + onRead: { + wifiScanning = false; + getNetworks.running = true; + } + } + } + // Status update function update() { updateConnectionType.startCheck(); @@ -118,4 +211,78 @@ Singleton { } } } + + Process { + id: getNetworks + running: true + command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"] + environment: ({ + LANG: "C", + LC_ALL: "C" + }) + stdout: StdioCollector { + onStreamFinished: { + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = text.trim().split("\n").map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1]), + frequency: parseInt(net[2]), + ssid: net[3], + bssid: net[4]?.replace(rep2, ":") ?? "", + security: net[5] || "" + }; + }).filter(n => n.ssid && n.ssid.length > 0); + + // Group networks by SSID and prioritize connected ones + const networkMap = new Map(); + for (const network of allNetworks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + // Prioritize active/connected networks + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + // If both are inactive, keep the one with better signal + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + // If existing is active and new is not, keep existing + } + } + + const wifiNetworks = Array.from(networkMap.values()); + + const rNetworks = root.wifiNetworks; + + const destroyed = rNetworks.filter(rn => !wifiNetworks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); + for (const network of destroyed) + rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy()); + + for (const network of wifiNetworks) { + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + } + } + } + + Component { + id: apComp + + WifiAccessPoint {} + } } diff --git a/.config/quickshell/ii/services/network/WifiAccessPoint.qml b/.config/quickshell/ii/services/network/WifiAccessPoint.qml new file mode 100644 index 000000000..55ee811f5 --- /dev/null +++ b/.config/quickshell/ii/services/network/WifiAccessPoint.qml @@ -0,0 +1,14 @@ +import QtQuick + +QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + + property bool askingPassword: false +} From 8c737f2ca436f997d8e3eb5883ceb31f6fecc328 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:59:35 +0700 Subject: [PATCH 36/68] quickshell: fix some warnings --- .config/quickshell/ii/modules/background/Background.qml | 2 +- .config/quickshell/ii/modules/bar/BarContent.qml | 4 ++-- .../quickshell/ii/modules/screenCorners/ScreenCorners.qml | 2 +- .../ii/modules/sidebarRight/CenterWidgetGroup.qml | 5 ++++- .../modules/sidebarRight/quickToggles/BluetoothToggle.qml | 6 +++--- .../ii/modules/verticalBar/VerticalBarContent.qml | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index 31d7acba4..5df366d93 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -29,7 +29,7 @@ Variants { // Hide when fullscreen property list workspacesForMonitor: Hyprland.workspaces.values.filter(workspace=>workspace.monitor && workspace.monitor.name == monitor.name) - property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland.fullscreen)[0] != undefined) && workspace.active))[0] + property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0] visible: GlobalStates.screenLocked || (!(activeWorkspaceWithFullscreen != undefined)) || !Config?.options.background.hideWhenFullscreen // Workspaces diff --git a/.config/quickshell/ii/modules/bar/BarContent.qml b/.config/quickshell/ii/modules/bar/BarContent.qml index acc06452f..d9670b642 100644 --- a/.config/quickshell/ii/modules/bar/BarContent.qml +++ b/.config/quickshell/ii/modules/bar/BarContent.qml @@ -307,8 +307,8 @@ Item { // Bar content region color: rightSidebarButton.colText } MaterialSymbol { - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected) + readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled + readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) readonly property bool bluetoothConnected: bluetoothDevice !== undefined text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" iconSize: Appearance.font.pixelSize.larger diff --git a/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml b/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml index 4e3102fb0..f62794f47 100644 --- a/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml +++ b/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml @@ -126,7 +126,7 @@ Scope { // Hide when fullscreen property list workspacesForMonitor: Hyprland.workspaces.values.filter(workspace => workspace.monitor && workspace.monitor.name == monitor.name) - property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland.fullscreen)[0] != undefined) && workspace.active))[0] + property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0] property bool fullscreen: activeWorkspaceWithFullscreen != undefined CornerPanelWindow { diff --git a/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml b/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml index 65b80d886..dccf89456 100644 --- a/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml +++ b/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml @@ -15,7 +15,10 @@ Rectangle { color: Appearance.colors.colLayer1 property int selectedTab: 0 - property var tabButtonList: [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Audio")}] + property var tabButtonList: [ + {"icon": "notifications", "name": Translation.tr("Notifications")}, + {"icon": "volume_up", "name": Translation.tr("Audio")} + ] Keys.onPressed: (event) => { if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) { diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml index 6bd4651dc..d1ad1c3a6 100644 --- a/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml @@ -10,13 +10,13 @@ import Quickshell.Hyprland QuickToggleButton { id: root - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected) + readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled ?? false + readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null readonly property bool bluetoothConnected: bluetoothDevice !== undefined toggled: bluetoothEnabled buttonIcon: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" onClicked: { - Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled + Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled } altAction: () => { Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]) diff --git a/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml b/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml index 2245457c1..66ccd78f4 100644 --- a/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml +++ b/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml @@ -285,8 +285,8 @@ Item { // Bar content region color: rightSidebarButton.colText } MaterialSymbol { - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected) + readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled + readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) readonly property bool bluetoothConnected: bluetoothDevice !== undefined text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" iconSize: Appearance.font.pixelSize.larger From c1b56922aa48597b188e6ff6c442118d0d08725f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:30:58 +0700 Subject: [PATCH 37/68] sidebar: refractor wifi dialog to new file, make it dynamically loaded --- .../sidebarRight/SidebarRightContent.qml | 90 +++++-------------- .../sidebarRight/wifiNetworks/WifiDialog.qml | 76 ++++++++++++++++ .../wifiNetworks/WifiNetworkItem.qml | 15 ++-- 3 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 .config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 88440d11a..f02a2b8a1 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -20,14 +20,13 @@ Item { property int sidebarWidth: Appearance.sizes.sidebarWidth property int sidebarPadding: 12 property string settingsQmlPath: Quickshell.shellPath("settings.qml") - property bool showDialog: false - property bool dialogIsWifi: true + property bool showWifiDialog: false Connections { target: GlobalStates function onSidebarRightOpenChanged() { if (!GlobalStates.sidebarRightOpen) { - root.showDialog = false + root.showWifiDialog = false; } } } @@ -86,8 +85,8 @@ Item { toggled: false buttonIcon: "restart_alt" onClicked: { - Hyprland.dispatch("reload") - Quickshell.reload(true) + Hyprland.dispatch("reload"); + Quickshell.reload(true); } StyledToolTip { content: Translation.tr("Reload Hyprland & Quickshell") @@ -97,8 +96,8 @@ Item { toggled: false buttonIcon: "settings" onClicked: { - GlobalStates.sidebarRightOpen = false - Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]) + GlobalStates.sidebarRightOpen = false; + Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]); } StyledToolTip { content: Translation.tr("Settings") @@ -108,7 +107,7 @@ Item { toggled: false buttonIcon: "power_settings_new" onClicked: { - GlobalStates.sessionOpen = true + GlobalStates.sessionOpen = true; } StyledToolTip { content: Translation.tr("Session") @@ -125,10 +124,9 @@ Item { NetworkToggle { altAction: () => { - Network.enableWifi() - Network.rescanWifi() - root.dialogIsWifi = true - root.showDialog = true + Network.enableWifi(); + Network.rescanWifi(); + root.showWifiDialog = true; } } BluetoothToggle {} @@ -155,65 +153,21 @@ Item { } } - WindowDialog { - show: root.showDialog - onDismiss: root.showDialog = false - anchors { - fill: parent - } - - WindowDialogTitle { - text: Translation.tr("Connect to Wi-Fi") - } - WindowDialogSeparator { - // TODO: add indeterminate progress bar when scanning - } - StyledListView { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.topMargin: -15 - Layout.bottomMargin: -16 - Layout.leftMargin: -Appearance.rounding.large - Layout.rightMargin: -Appearance.rounding.large - - clip: true - spacing: 0 - animateAppearance: false + onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true; + Loader { + id: wifiDialogLoader + anchors.fill: parent - model: ScriptModel { - values: [...Network.wifiNetworks].sort((a, b) => { - if (a.active && !b.active) return -1; - if (!a.active && b.active) return 1; - return b.strength - a.strength; - }) - } - // model: Network.wifiNetworks - delegate: WifiNetworkItem { - required property WifiAccessPoint modelData - wifiNetwork: modelData - anchors { - left: parent?.left - right: parent?.right - } - } - } - WindowDialogSeparator {} - WindowDialogButtonRow { - DialogButton { - buttonText: Translation.tr("Details") - onClicked: { - Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]) - GlobalStates.sidebarRightOpen = false - } - } + active: root.showWifiDialog || item.visible + onActiveChanged: if (active) item.show = true - Item { - Layout.fillWidth: true + sourceComponent: WifiDialog { + onDismiss: { + show = false + root.showWifiDialog = false } - - DialogButton { - buttonText: Translation.tr("Done") - onClicked: root.showDialog = false + onVisibleChanged: { + if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false; } } } diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml new file mode 100644 index 000000000..d7e9dd723 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml @@ -0,0 +1,76 @@ +import qs +import qs.services +import qs.services.network +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import "./quickToggles/" +import "./wifiNetworks/" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell.Io +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland + +WindowDialog { + id: root + + WindowDialogTitle { + text: Translation.tr("Connect to Wi-Fi") + } + // TODO: add indeterminate progress bar when scanning + WindowDialogSeparator {} + StyledListView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -15 + Layout.bottomMargin: -16 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + + clip: true + spacing: 0 + animateAppearance: false + + model: ScriptModel { + values: [...Network.wifiNetworks].sort((a, b) => { + if (a.active && !b.active) + return -1; + if (!a.active && b.active) + return 1; + return b.strength - a.strength; + }) + } + // model: Network.wifiNetworks + delegate: WifiNetworkItem { + required property WifiAccessPoint modelData + wifiNetwork: modelData + anchors { + left: parent?.left + right: parent?.right + } + } + } + WindowDialogSeparator {} + WindowDialogButtonRow { + DialogButton { + buttonText: Translation.tr("Details") + onClicked: { + Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]); + GlobalStates.sidebarRightOpen = false; + } + } + + Item { + Layout.fillWidth: true + } + + DialogButton { + buttonText: Translation.tr("Done") + onClicked: root.dismiss() + } + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index 907ec9402..733c04ef1 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -45,20 +45,21 @@ RippleButton { spacing: 10 MaterialSymbol { iconSize: Appearance.font.pixelSize.larger - text: root.wifiNetwork?.strength > 80 ? "signal_wifi_4_bar" : - root.wifiNetwork?.strength > 60 ? "network_wifi_3_bar" : - root.wifiNetwork?.strength > 40 ? "network_wifi_2_bar" : - root.wifiNetwork?.strength > 20 ? "network_wifi_1_bar" : + property int strength: root.wifiNetwork?.strength ?? 0 + text: strength > 80 ? "signal_wifi_4_bar" : + strength > 60 ? "network_wifi_3_bar" : + strength > 40 ? "network_wifi_2_bar" : + strength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar" color: Appearance.colors.colOnSurfaceVariant } StyledText { Layout.fillWidth: true - text: root.wifiNetwork?.ssid + text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") color: Appearance.colors.colOnSurfaceVariant } MaterialSymbol { - visible: root.wifiNetwork?.isSecure || root.wifiNetwork?.active + visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock" iconSize: Appearance.font.pixelSize.larger color: Appearance.colors.colOnSurfaceVariant @@ -67,7 +68,7 @@ RippleButton { ColumnLayout { id: passwordPrompt - visible: root.wifiNetwork?.askingPassword + visible: root.wifiNetwork?.askingPassword ?? false Layout.topMargin: 12 MaterialTextField { From 2ad304aaf28046a0a89b6b1270f843755334f52b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:13:19 +0700 Subject: [PATCH 38/68] config: rename "mantra" to "quote" --- .config/quickshell/ii/modules/common/Config.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index d70199550..06d593bd2 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -127,7 +127,7 @@ Singleton { property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size property bool enableSidebar: true } - property string mantra: "" + property string quote: "" property bool hideWhenFullscreen: true } From fc69ca059917bdaee8b934fcc5ca4891c33f037a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:13:46 +0700 Subject: [PATCH 39/68] sidebar: wifi dialog: esc to close --- .../quickshell/ii/modules/common/widgets/WindowDialog.qml | 7 +++++++ .../ii/modules/sidebarRight/SidebarRightContent.qml | 7 ++++++- .../ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml index 084f0bbed..d4f9f3285 100644 --- a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml @@ -12,7 +12,14 @@ Rectangle { default property alias data: contentColumn.data property real backgroundHeight: 600 property real backgroundAnimationMovementDistance: 60 + signal dismiss() + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + root.dismiss(); + event.accepted = true; + } + } color: root.show ? Appearance.colors.colScrim : ColorUtils.transparentize(Appearance.colors.colScrim) Behavior on color { diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index f02a2b8a1..37878a669 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -159,7 +159,12 @@ Item { anchors.fill: parent active: root.showWifiDialog || item.visible - onActiveChanged: if (active) item.show = true + onActiveChanged: { + if (active) { + item.show = true; + item.forceActiveFocus(); + } + } sourceComponent: WifiDialog { onDismiss: { diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml index d7e9dd723..6569704a8 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml @@ -17,7 +17,7 @@ import Quickshell.Hyprland WindowDialog { id: root - + WindowDialogTitle { text: Translation.tr("Connect to Wi-Fi") } From e85f59db8c14acdcdb764ec57f8a1bda4454ae90 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:18:17 +0700 Subject: [PATCH 40/68] remove useless comments --- .../quickshell/ii/modules/common/widgets/RoundCorner.qml | 2 +- .config/quickshell/ii/services/Booru.qml | 2 +- .config/quickshell/ii/services/Hyprsunset.qml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml b/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml index a2177a13b..4dbbb56e6 100644 --- a/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml +++ b/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml @@ -5,7 +5,7 @@ Item { id: root enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight } - property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft + property var corner: RoundCorner.CornerEnum.TopLeft property int implicitSize: 25 property color color: "#000000" diff --git a/.config/quickshell/ii/services/Booru.qml b/.config/quickshell/ii/services/Booru.qml index 862542cc6..07fbd210f 100644 --- a/.config/quickshell/ii/services/Booru.qml +++ b/.config/quickshell/ii/services/Booru.qml @@ -259,7 +259,7 @@ Singleton { // Alcy doesn't provide dimensions and images are often of god resolution "width": 1000, "height": 1000, - "aspect_ratio": 1, // Default aspect ratio + "aspect_ratio": 1, "tags": "[no tags]", "rating": "s", "is_nsfw": false, diff --git a/.config/quickshell/ii/services/Hyprsunset.qml b/.config/quickshell/ii/services/Hyprsunset.qml index 8b309a8d0..3f33aa549 100644 --- a/.config/quickshell/ii/services/Hyprsunset.qml +++ b/.config/quickshell/ii/services/Hyprsunset.qml @@ -13,10 +13,10 @@ import Quickshell.Io Singleton { id: root property var manualActive - property string from: Config.options?.light?.night?.from ?? "19:00" // Default to 7 PM - property string to: Config.options?.light?.night?.to ?? "06:30" // Default to 6:30 AM + property string from: Config.options?.light?.night?.from ?? "19:00" + property string to: Config.options?.light?.night?.to ?? "06:30" property bool automatic: Config.options?.light?.night?.automatic && (Config?.ready ?? true) - property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000 // Default color temperature + property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000 property bool shouldBeOn property bool firstEvaluation: true property bool active: false From 1c1a1417015741ed13d195c38b8b2844d627ea5f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:51:15 +0700 Subject: [PATCH 41/68] wallpaper selector: freedesktop spec-compliant thumbnail generation --- .../ii/modules/common/Appearance.qml | 2 + .../quickshell/ii/modules/common/Images.qml | 19 +++ .../ii/modules/common/ThumbnailImage.qml | 26 ++--- .../WallpaperDirectoryItem.qml | 28 +++-- .../WallpaperSelectorContent.qml | 19 ++- .../ii/scripts/thumbnails/thumbgen.py | 109 ++++++++++++++++++ .config/quickshell/ii/services/Wallpapers.qml | 22 ++++ scriptdata/requirements.in | 5 + scriptdata/requirements.txt | 12 ++ 9 files changed, 211 insertions(+), 31 deletions(-) create mode 100755 .config/quickshell/ii/scripts/thumbnails/thumbgen.py diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index ddcbb3ca9..f1a8daec0 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -345,6 +345,8 @@ Singleton { (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" diff --git a/.config/quickshell/ii/modules/common/Images.qml b/.config/quickshell/ii/modules/common/Images.qml index ac76f5118..be3701efc 100644 --- a/.config/quickshell/ii/modules/common/Images.qml +++ b/.config/quickshell/ii/modules/common/Images.qml @@ -3,10 +3,29 @@ pragma Singleton import Quickshell Singleton { + // Formats readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] readonly property list 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"; + } } diff --git a/.config/quickshell/ii/modules/common/ThumbnailImage.qml b/.config/quickshell/ii/modules/common/ThumbnailImage.qml index a1a4c204f..ee928eef1 100644 --- a/.config/quickshell/ii/modules/common/ThumbnailImage.qml +++ b/.config/quickshell/ii/modules/common/ThumbnailImage.qml @@ -5,28 +5,15 @@ import qs.modules.common import qs.modules.common.functions /** - * Thumbnail image. + * 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 - readonly property var thumbnailSizes: ({ - "normal": 128, - "large": 256, - "x-large": 512, - "xx-large": 1024 - }) - property string thumbnailSizeName: { // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html - const sizeNames = Object.keys(thumbnailSizes); - for(let i = 0; i < sizeNames.length; i++) { - const sizeName = sizeNames[i]; - const maxSize = thumbnailSizes[sizeName]; - if (root.sourceSize.width <= maxSize && root.sourceSize.height <= maxSize) return sizeName; - } - return "xx-large"; - } + property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height) property string thumbnailPath: { if (sourcePath.length == 0) return; const resolvedUrl = Qt.resolvedUrl(sourcePath); @@ -46,13 +33,14 @@ Image { } onSourceSizeChanged: { - thumbnailGeneration.running = false - thumbnailGeneration.running = true + if (!root.generateThumbnail) return; + thumbnailGeneration.running = false; + thumbnailGeneration.running = true; } Process { id: thumbnailGeneration command: { - const maxSize = root.thumbnailSizes[root.thumbnailSizeName]; + 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; }` ] diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 2616219c8..0b3f877f4 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -19,19 +19,19 @@ MouseArea { property alias colBackground: background.color property alias colText: wallpaperItemName.color property alias radius: background.radius - property alias padding: background.anchors.margins + property alias margins: background.anchors.margins + property alias padding: wallpaperItemColumnLayout.anchors.margins + margins: Appearance.sizes.wallpaperSelectorItemMargins + padding: Appearance.sizes.wallpaperSelectorItemPadding - signal activated + signal activated() hoverEnabled: true onClicked: root.activated() Rectangle { id: background - anchors { - fill: parent - margins: 8 - } + anchors.fill: parent radius: Appearance.rounding.normal Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) @@ -39,10 +39,7 @@ MouseArea { ColumnLayout { id: wallpaperItemColumnLayout - anchors { - fill: parent - margins: 6 - } + anchors.fill: parent spacing: 4 Item { @@ -67,6 +64,7 @@ MouseArea { active: root.useThumbnail sourceComponent: ThumbnailImage { id: thumbnailImage + generateThumbnail: false sourcePath: fileModelData.filePath fillMode: Image.PreserveAspectCrop @@ -74,6 +72,16 @@ MouseArea { 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 { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 70d038ae1..b55233417 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -16,6 +16,19 @@ Item { 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; @@ -203,9 +216,12 @@ Item { 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); @@ -219,7 +235,6 @@ Item { model: Wallpapers.folderModel onModelChanged: currentIndex = 0 - delegate: WallpaperDirectoryItem { required property var modelData required property int index diff --git a/.config/quickshell/ii/scripts/thumbnails/thumbgen.py b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py new file mode 100755 index 000000000..4dc3ccd56 --- /dev/null +++ b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py @@ -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() diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d1c211518..ebd7c27e5 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -14,6 +14,7 @@ pragma ComponentBehavior: Bound 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: "" @@ -23,6 +24,7 @@ Singleton { property list wallpapers: [] // List of absolute file paths (without file://) signal changed() + signal thumbnailGenerated(directory: string) // Executions Process { @@ -105,4 +107,24 @@ Singleton { } } } + + // 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) + } + } } diff --git a/scriptdata/requirements.in b/scriptdata/requirements.in index 0b4ac323e..26a028973 100644 --- a/scriptdata/requirements.in +++ b/scriptdata/requirements.in @@ -8,3 +8,8 @@ materialyoucolor libsass material-color-utilities setproctitle +click +loguru +pycairo +pygobject +tqdm diff --git a/scriptdata/requirements.txt b/scriptdata/requirements.txt index c2f380c4a..e6b96aba1 100644 --- a/scriptdata/requirements.txt +++ b/scriptdata/requirements.txt @@ -4,8 +4,12 @@ build==1.2.2.post1 # via -r scriptdata/requirements.in cffi==1.17.1 # via pywayland +click==8.2.1 + # via -r scriptdata/requirements.in libsass==0.23.0 # via -r scriptdata/requirements.in +loguru==0.7.3 + # via -r scriptdata/requirements.in material-color-utilities==0.2.1 # via -r scriptdata/requirements.in materialyoucolor==2.0.10 @@ -22,8 +26,14 @@ pillow==11.1.0 # material-color-utilities psutil==6.1.1 # via -r scriptdata/requirements.in +pycairo==1.28.0 + # via + # -r scriptdata/requirements.in + # pygobject pycparser==2.22 # via cffi +pygobject==3.52.3 + # via -r scriptdata/requirements.in pyproject-hooks==1.2.0 # via build pywayland==0.4.18 @@ -34,5 +44,7 @@ setuptools==80.9.0 # via setuptools-scm setuptools-scm==8.1.0 # via -r scriptdata/requirements.in +tqdm==4.67.1 + # via -r scriptdata/requirements.in wheel==0.45.1 # via -r scriptdata/requirements.in From f74ed76850bba8b7cc6414e782f97177eb53b0c6 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:00:48 +0700 Subject: [PATCH 42/68] cheatsheet: use slide anim --- .config/hypr/hyprland/rules.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/hypr/hyprland/rules.conf b/.config/hypr/hyprland/rules.conf index 236be8507..f5a86a238 100644 --- a/.config/hypr/hyprland/rules.conf +++ b/.config/hypr/hyprland/rules.conf @@ -137,6 +137,7 @@ layerrule = animation slide left, quickshell:sidebarLeft layerrule = animation slide top, quickshell:wallpaperSelector layerrule = animation slide bottom, quickshell:osk layerrule = animation slide bottom, quickshell:dock +layerrule = animation slide bottom, quickshell:cheatsheet layerrule = blur, quickshell:session layerrule = noanim, quickshell:session layerrule = ignorealpha 0, quickshell:session From 28c9cacf68c8b47216b1f6cec3cd142d992bc2be Mon Sep 17 00:00:00 2001 From: kirisaki-vk Date: Thu, 28 Aug 2025 09:30:54 +0300 Subject: [PATCH 43/68] bar: add workspace number mapping --- .config/quickshell/ii/modules/bar/Workspaces.qml | 2 +- .config/quickshell/ii/modules/common/Config.qml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/bar/Workspaces.qml b/.config/quickshell/ii/modules/bar/Workspaces.qml index ace453ba0..d3ded59b8 100644 --- a/.config/quickshell/ii/modules/bar/Workspaces.qml +++ b/.config/quickshell/ii/modules/bar/Workspaces.qml @@ -217,7 +217,7 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) - text: `${button.workspaceValue}` + text: `${Config.options?.bar.workspaces.numberMapping[button.workspaceValue - 1] || button.workspaceValue}` elide: Text.ElideRight color: (monitor?.activeWorkspace?.id == button.workspaceValue) ? Appearance.m3colors.m3onPrimary : diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 06d593bd2..c8a8bc78a 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -174,6 +174,7 @@ Singleton { property bool showAppIcons: true property bool alwaysShowNumbers: false property int showNumberDelay: 300 // milliseconds + property list numberMapping: [] } property JsonObject weather: JsonObject { property bool enable: false From b52440bcc10328e5999be4f79fb72745fcc7a249 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Thu, 28 Aug 2025 18:17:11 +0700 Subject: [PATCH 44/68] media controls: outside click dismissal, no player placeholder --- .../modules/mediaControls/MediaControls.qml | 70 +++++++++++++++---- .../modules/mediaControls/PlayerControl.qml | 5 +- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/.config/quickshell/ii/modules/mediaControls/MediaControls.qml b/.config/quickshell/ii/modules/mediaControls/MediaControls.qml index 41347d5ce..b5742075a 100644 --- a/.config/quickshell/ii/modules/mediaControls/MediaControls.qml +++ b/.config/quickshell/ii/modules/mediaControls/MediaControls.qml @@ -40,37 +40,34 @@ Scope { } return ( // Remove unecessary native buses from browsers if there's plasma integration - !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && - !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) && + !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) && // playerctld just copies other buses and we don't need duplicates !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && // Non-instance mpd bus - !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')) - ); + !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd'))); } function filterDuplicatePlayers(players) { let filtered = []; let used = new Set(); for (let i = 0; i < players.length; ++i) { - if (used.has(i)) continue; + if (used.has(i)) + continue; let p1 = players[i]; let group = [i]; // Find duplicates by trackTitle prefix for (let j = i + 1; j < players.length; ++j) { let p2 = players[j]; - if (p1.trackTitle && p2.trackTitle && - (p1.trackTitle.includes(p2.trackTitle) - || p2.trackTitle.includes(p1.trackTitle)) - || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) { + if (p1.trackTitle && p2.trackTitle && (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle)) || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) { group.push(j); } } // Pick the one with non-empty trackArtUrl, or fallback to the first let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0); - if (chosenIdx === undefined) chosenIdx = group[0]; + if (chosenIdx === undefined) + chosenIdx = group[0]; filtered.push(players[chosenIdx]); group.forEach(idx => used.add(idx)); @@ -133,6 +130,16 @@ Scope { item: playerColumnLayout } + HyprlandFocusGrab { + windows: [mediaControlsRoot] + active: mediaControlsLoader.active + onCleared: () => { + if (!active) { + GlobalStates.mediaControlsOpen = false; + } + } + } + ColumnLayout { id: playerColumnLayout anchors.fill: parent @@ -148,6 +155,43 @@ Scope { visualizerPoints: root.visualizerPoints implicitWidth: widgetWidth implicitHeight: widgetHeight + radius: root.popupRounding + } + } + + Item { // No player placeholder + Layout.fillWidth: true + visible: root.meaningfulPlayers.length === 0 + implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin + implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin + + StyledRectangularShadow { + target: placeholderBackground + } + + Rectangle { + id: placeholderBackground + anchors.centerIn: parent + color: Appearance.colors.colLayer0 + radius: root.popupRounding + property real padding: 20 + implicitWidth: placeholderLayout.implicitWidth + padding * 2 + implicitHeight: placeholderLayout.implicitHeight + padding * 2 + + ColumnLayout { + id: placeholderLayout + anchors.centerIn: parent + + StyledText { + text: Translation.tr("No active player") + font.pixelSize: Appearance.font.pixelSize.large + } + StyledText { + color: Appearance.colors.colSubtext + text: Translation.tr("Make sure your player has MPRIS support\nor try turning off duplicate player filtering") + font.pixelSize: Appearance.font.pixelSize.small + } + } } } } @@ -159,7 +203,8 @@ Scope { function toggle(): void { mediaControlsLoader.active = !mediaControlsLoader.active; - if(mediaControlsLoader.active) Notifications.timeoutAll(); + if (mediaControlsLoader.active) + Notifications.timeoutAll(); } function close(): void { @@ -196,5 +241,4 @@ Scope { GlobalStates.mediaControlsOpen = false; } } - -} \ No newline at end of file +} diff --git a/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml index b10781d2b..d83b85771 100644 --- a/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml +++ b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml @@ -22,6 +22,7 @@ Item { // Player instance property list visualizerPoints: [] property real maxVisualizerValue: 1000 // Max value in the data points property int visualizerSmoothing: 2 // Number of points to average for smoothing + property real radius component TrackChangeButton: RippleButton { implicitWidth: 24 @@ -107,7 +108,7 @@ Item { // Player instance anchors.fill: parent anchors.margins: Appearance.sizes.elevationMargin color: blendedColors.colLayer0 - radius: root.popupRounding + radius: playerController.radius layer.enabled: true layer.effect: OpacityMask { @@ -141,7 +142,7 @@ Item { // Player instance Rectangle { anchors.fill: parent color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3) - radius: root.popupRounding + radius: playerController.radius } } From 9ca80d352bc05748fc0e4b60315ba2322ceb9324 Mon Sep 17 00:00:00 2001 From: rain022 Date: Fri, 29 Aug 2025 04:38:12 +0900 Subject: [PATCH 45/68] fix: improve Japanese translations for clarity and consistency --- .config/quickshell/translations/ja_JP.json | 222 +++++++++++++++------ 1 file changed, 158 insertions(+), 64 deletions(-) diff --git a/.config/quickshell/translations/ja_JP.json b/.config/quickshell/translations/ja_JP.json index 4b75fe30f..7faf042a1 100644 --- a/.config/quickshell/translations/ja_JP.json +++ b/.config/quickshell/translations/ja_JP.json @@ -7,27 +7,26 @@ "Sa": "土/*keep*/", "Su": "日/*keep*/", "%1 characters": "%1 文字", - "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**料金**: 無料。データ利用ポリシーはOpenRouterアカウント設定によって異なります。\n\n**手順**: OpenRouterアカウントにログインし、右上メニューのKeysに進み、Create API Keyをクリックしてください。", - "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**料金**: 無料。データは学習に使用されます。\n\n**手順**: Googleアカウントにログインし、AI StudioにGoogle Cloudプロジェクトの作成などを許可し、戻ってAPIキーを取得してください。", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**料金**: 無料。データ利用ポリシーはアカウント設定によって異なります。\n\n**手順**: OpenRouterにログイン後、右上の「Keys」メニューから「Create API Key」をクリックしてください。", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**料金**: 無料。データは学習に使用されます。\n\n**手順**: Googleアカウントにログインし、AI StudioにGoogle Cloudプロジェクトなどの作成を許可した後、戻って「APIキーを取得」をクリックしてください。", ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": "Zerochanの注意:\n- 色を入力する必要があります\n- `sidebar.booru.zerochan.username`設定でユーザー名を設定してください。設定しないと[利用停止になる場合があります](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", - "No further instruction provided": "追加の指示はありません", + "No further instruction provided": "追加の指示がありません", "Action": "操作", "Add": "追加", "Add task": "タスクを追加", - "All-rounder | Good quality, decent quantity": "万能型 | 品質良好、量も十分", + "All-rounder | Good quality, decent quantity": "オールラウンド | 良質で十分な量", "Allow NSFW": "NSFWを許可", "Allow NSFW content": "NSFWコンテンツを許可", "Anime": "アニメ", "Anime boorus": "アニメ画像掲示板", "App": "アプリ", - "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEscまたはどこかをクリックでキャンセル", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEscキーか画面をクリックでキャンセル", "Bluetooth": "Bluetooth", - "Brightness": "明るさ", + "Brightness": "画面の明るさ", "Cancel": "キャンセル", - "Chain of Thought": "思考プロセス", "Cheat sheet": "チートシート", "Choose model": "モデルを選択", - "Clean stuff | Excellent quality, no NSFW": "健全系 | 高品質、NSFWなし", + "Clean stuff | Excellent quality, no NSFW": "健全コンテンツ | 最高品質、成人向けなし", "Clear": "クリア", "Clear chat history": "チャット履歴を消去", "Clear the current list of images": "現在の画像リストをクリア", @@ -42,8 +41,8 @@ "Edit": "編集", "Enter text to translate...": "翻訳するテキストを入力...", "Finished tasks will go here": "完了したタスクはここに表示されます", - "For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 良質", - "For storing API keys and other sensitive information": "APIキーや機密情報の保存用", + "For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 高品質", + "For storing API keys and other sensitive information": "APIキーなどの機密情報を安全に保管", "Game mode": "ゲームモード", "Get the next page of results": "次のページを取得", "Hibernate": "休止", @@ -53,14 +52,13 @@ "Invalid arguments. Must provide `key` and `value`.": "無効な引数です。`key`と`value`を指定してください。", "Jump to current month": "現在の月へ移動", "Keep system awake": "システムをスリープさせない", - "Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質、NSFWなし。", + "Large images | God tier quality, no NSFW.": "大型画像 | 最上級の品質、成人向けなし", "Large language models": "大規模言語モデル", "Launch": "起動", "Lock": "ロック", "Logout": "ログアウト", "Markdown test": "Markdownテスト", "Math result": "計算結果", - "Night Light": "夜間モード", "No audio source": "音声ソースなし", "No media": "メディアなし", "No notifications": "通知なし", @@ -83,17 +81,17 @@ "Select Language": "言語を選択", "Session": "セッション", "Set API key": "APIキーを設定", - "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、他は0~1。デフォルトは0.5。", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、その他のモデルは0~1の範囲で指定可能。デフォルトは0.5です。", "Set the current API provider": "現在のAPIプロバイダーを設定します", "Shutdown": "シャットダウン", - "Silent": "サイレント", + "Silent": "サイレントモード", "Sleep": "スリープ", "System": "システム", "Task Manager": "タスクマネージャー", "Task description": "タスクの説明", "Temperature must be between 0 and 2": "温度は0~2の間で指定してください", - "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "エロ系 | 量が多くNSFW多数、品質はバラバラ", - "The popular one | Best quantity, but quality can vary wildly": "人気系 | 量は最多、品質はバラつきあり", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向け | 大量にあり多くがアダルト、品質は様々", + "The popular one | Best quantity, but quality can vary wildly": "人気コンテンツ | 最多の量だが品質は様々", "Thinking": "考え中", "Translation goes here...": "ここに翻訳が表示されます...", "Translator": "翻訳", @@ -105,20 +103,16 @@ "View Markdown source": "Markdownソースを表示", "Volume": "音量", "Volume mixer": "音量ミキサー", - "Waifus only | Excellent quality, limited quantity": "美少女系のみ | 高品質、量は少なめ", + "Waifus only | Excellent quality, limited quantity": "美少女キャラのみ | 高品質だが少量", "Waiting for response...": "応答待ち...", "Workspace": "ワークスペース", - "Set with /mode PROVIDER": "/mode PROVIDER で設定", "Invalid API provider. Supported: \n-": "無効なAPIプロバイダー。対応: \n-", "Unknown command:": "不明なコマンド:", - "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/keyでオンラインモデルを開始\nCtrl+Oでサイドバー展開\nCtrl+Pでサイドバーをウィンドウ化", - "The current API used. Endpoint:": "現在使用中のAPIのエンドポイント:", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/keyでオンラインモデルを開始\nCtrl+Oでサイドバーを開く\nCtrl+Pでサイドバーを別窓に", "Provider set to": "プロバイダーを設定しました:", "Invalid model. Supported: \n```": "無効なモデル。対応: \n```", - "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "うまくいきませんでした。ヒント:\n- タグやNSFW設定を確認\n- タグがなければページ番号を入力", - "Online | Google's model\nGives up-to-date information with search.": "オンライン | Googleのモデル\n検索で最新情報を取得します", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "検索に失敗しました。ヒント:\n- タグやNSFW設定を確認してください\n- 特定のタグがなければ、ページ番号を入力してみてください", "Switched to search mode. Continue with the user's request.": "検索モードに切り替えました。リクエストを続行します。", - "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "実験的 | オンライン | Googleのモデル\n少し多機能ですが、検索は遅めです", "Settings": "設定", "Save chat": "チャットを保存", "Load chat": "チャットを読み込み", @@ -130,22 +124,21 @@ "About": "このアプリについて", "Services": "サービス", "Style": "スタイル", - "Edit config": "設定を編集", "Colors & Wallpaper": "色と壁紙", "Light": "ライト", "Dark": "ダーク", - "Material palette": "マテリアルパレット", - "Fidelity": "忠実度", - "Fruit Salad": "フルーツサラダ", + "Material palette": "カラーパレット", + "Fidelity": "精度", + "Fruit Salad": "フルーツカラー", "Alternatively use /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgも利用可能", - "Fake screen rounding": "画面の角を疑似的に丸める", - "When not fullscreen": "全画面でない場合", + "Fake screen rounding": "画面の角を疑似的に丸くする", + "When not fullscreen": "全画面表示以外のとき", "Choose file": "ファイルを選択", - "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "KonachanからランダムなSFW(健全)アニメ壁紙\n画像は~/Pictures/Wallpapersに保存されます", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Konachanから一般向けアニメ壁紙をランダム取得\n画像は~/Pictures/Wallpapersに保存", "Be patient...": "しばらくお待ちください...", - "Decorations & Effects": "装飾と効果", - "Tonal Spot": "トーナルスポット", - "Shell windows": "シェルウィンドウ", + "Decorations & Effects": "装飾とエフェクト", + "Tonal Spot": "色調スポット", + "Shell windows": "ウィンドウ装飾", "Auto": "自動", "Wallpaper": "壁紙", "Content": "コンテンツ", @@ -155,7 +148,7 @@ "Yes": "はい", "Enable": "有効化", "Rainbow": "レインボー", - "Might look ass. Unsupported.": "見た目が悪くなる場合があります(未サポート)", + "Might look ass. Unsupported.": "見た目が悪くなる可能性あり(非対応)", "Monochrome": "モノクロ", "Random: Konachan": "ランダム: Konachan", "Center title": "タイトルを中央に", @@ -165,21 +158,19 @@ "AI": "AI", "Local only": "ローカルのみ", "Policies": "ポリシー", - "Weeb": "オタク向け", + "Weeb": "アニメ・漫画趣味", "Closet": "クローゼット", - "Bar style": "バーのスタイル", "Show next time": "次回も表示する", "Usage": "使用状況", - "Plain rectangle": "ただの四角形", - "Useless buttons": "ダミーボタン", + "Plain rectangle": "通常の四角形", + "Useless buttons": "装飾ボタン", "GitHub": "GitHub", "Style & wallpaper": "スタイルと壁紙", "Configuration": "設定", "Change any time later with /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgでいつでも変更可能", "Keybinds": "キー割り当て", - "Float": "フローティング", - "Hug": "ハグ", - "Yooooo hi there": "やあ、こんにちは!", + "Float": "浮かせる", + "Hug": "内側に寄せる", "illogical-impulse Welcome": "illogical-impulseへようこそ", "Info": "情報", "Volume limit": "音量制限", @@ -194,8 +185,8 @@ "Battery": "バッテリー", "Prefixes": "接頭辞", "Emojis": "絵文字", - "Earbang protection": "急音防止", - "Automatically suspends the system when battery is low": "バッテリー残量が少ないときに自動でスリープします", + "Earbang protection": "急な音量上昇防止", + "Automatically suspends the system when battery is low": "バッテリー残量が少なくなると自動的にシステムをスリープします", "Automatic suspend": "自動スリープ", "Suspend at": "スリープ開始残量(%)", "Max allowed increase": "最大許容増加幅", @@ -204,21 +195,20 @@ "Clipboard": "クリップボード", "Low warning": "バッテリー低下の警告", "24h": "24時間", - "Use Levenshtein distance-based algorithm instead of fuzzy": "ファジー検索の代わりにレーベンシュタイン距離ベースのアルゴリズムを使用", + "Use Levenshtein distance-based algorithm instead of fuzzy": "あいまい検索の代わりに文字間の類似度アルゴリズムを使用", "System prompt": "システムプロンプト", "12h AM/PM": "12時間(AM/PM)", - "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "タイプミスが多い場合は便利ですが、結果が変になることもあり、略語には対応しない場合があります\n(例: \"GIMP\"でペイントソフトが出ないことも)", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "タイプミスが多い場合に役立ちますが、検索結果が不自然になることがあり、頭字語では機能しないことも\n(例:「GIMP」で画像編集ソフトが検索できないなど)", "Critical warning": "重大な警告", "User agent (for services that require it)": "ユーザーエージェント(必要なサービス用)", - "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらの領域は、画像や画面の一部など、一定のまとまりを持つ箇所を指します。\n常に正確とは限りません。\nこれはローカルで実行される画像処理アルゴリズムによるもので、AIは使用していません。", - "Note: turning off can hurt readability": "オフにすると可読性が下がる場合があります", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらは画像や画面上の一定のまとまりを持つ部分です。\n必ずしも正確ではない場合があります。\nローカルの画像処理アルゴリズムを使用しており、AIは使用していません。", + "Note: turning off can hurt readability": "注意:オフにすると読みやすさが低下する場合があります", "Workspaces shown": "表示中のワークスペース", "Dark/Light toggle": "ダーク/ライト切替", "Dock": "ドック", "Weather": "天気", "Pinned on startup": "起動時にピン留め", - "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを隠して常に数字を表示すると、クラシックなillogical-impulse体験になります", - "Appearance": "外観", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント:アイコンを非表示にして数字を常に表示すると\n従来のシンプルな操作感になります", "Always show numbers": "常に数字を表示", "Buttons": "ボタン", "Keyboard toggle": "キーボード切替", @@ -227,15 +217,15 @@ "Rows": "行数", "Borderless": "枠なし", "Screenshot tool": "スクリーンショットツール", - "Number show delay when pressing Super (ms)": "Superキー押下時の数字表示遅延(ms)", + "Number show delay when pressing Super (ms)": "Superキー押下時の数字表示遅延時間(ミリ秒)", "Timeout (ms)": "タイムアウト(ms)", "Show app icons": "アプリアイコンを表示", "Workspaces": "ワークスペース", "Columns": "列数", "On-screen display": "オンスクリーン表示", - "Screen snip": "画面切り取り", + "Screen snip": "画面キャプチャ", "Mic toggle": "マイク切替", - "Hover to reveal": "ホバーで表示", + "Hover to reveal": "マウスを近づけると表示", "Bar": "バー", "Show background": "背景を表示", "Show regions of potential interest": "注目領域を表示", @@ -266,11 +256,9 @@ "Current API endpoint: %1\nSet it with %2mode PROVIDER": "現在のAPIエンドポイント: %1\n%2mode PROVIDERで設定", "Go to source (%1)": "ソースを開く(%1)", "Temperature set to %1": "温度を%1に設定", - "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するにはコマンドで渡してください\n\nキーを表示するには「get」を指定してください
\n\n### %1の場合:\n\n**リンク**: %2\n\n%3", "Enter tags, or \"%1\" for commands": "タグを入力、またはコマンドは「%1」", "%1 queries pending": "%1件のクエリが保留中", "API key:\n\n```txt\n%1\n```": "APIキー:\n\n```txt\n%1\n```", - "Uptime: %1": "稼働時間: %1", "%1 Safe Storage": "%1 セーフストレージ", "%1 does not require an API key": "%1はAPIキー不要です", "Temperature: %1": "温度: %1", @@ -316,23 +304,129 @@ "Fully charged": "充電完了", "Charging:": "充電中:", "Discharging:": "放電中:", - "Uptime:": "稼働時間:", - "Upcoming Tasks:": "今後のタスク:", "No pending tasks": "保留中のタスクなし", "... and %1 more": "...他%1件", - "Memory Usage": "メモリ使用量", "Used:": "使用済み:", "Free:": "空き:", "Total:": "合計:", - "Usage:": "使用状況:", - "Swap Usage": "スワップ使用量", - "Swap:": "スワップ:", - "Not configured": "未設定", - "CPU Usage": "CPU使用率", - "Current:": "現在:", "Load:": "負荷:", "High": "高", "Medium": "中", "Low": "低", - "System Resource": "システムリソース" + "Shell conflicts killer": "シェル競合解決ツール", + "Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "オンライン | Googleのモデル\nコーディングや複雑な推論タスクに優れた、Googleの最先端多目的モデルです。", + "Pause": "一時停止", + "Vertical": "垂直", + "Tray": "トレイ", + "Tool set to: %1": "ツールを設定しました: %1", + "Stopwatch": "ストップウォッチ", + "Performance Profile toggle": "パフォーマンスプロファイル切替", + "Usage: %1tool TOOL_NAME": "使用法: %1tool ツール名", + "Focus": "集中モード", + "Pomodoro": "ポモドーロタイマー", + "API key is set\nChange with /key YOUR_API_KEY": "APIキーが設定されています\n/key APIキー で変更できます", + "Up %1": "稼働時間 %1", + "Config file": "設定ファイル", + "Automatically hide": "自動的に隠す", + "Refreshing (manually triggered)": "更新中(手動実行)", + "Resume": "再開", + "Enjoy! You can reopen the welcome app any time with Super+Shift+Alt+/. To open the settings app, hit Super+I": "ようこそ!ウェルカムアプリはSuper+Shift+Alt+/で再表示できます。設定アプリを開くにはSuper+Iを押してください", + "Long break": "長い休憩", + "To Do:": "やることリスト:", + "Invalid arguments. Must provide `command`.": "引数が無効です。`command`を指定してください。", + "Wallpaper parallax": "壁紙の視差効果", + "Break": "休憩", + "Open the shell config file.\nIf the button doesn't work or doesn't open in your favorite editor,\nyou can manually open ~/.config/illogical-impulse/config.json": "シェル設定ファイルを開きます。\nボタンが機能しないか、お好みのエディタで開かない場合は、\n手動で~/.config/illogical-impulse/config.jsonを開いてください", + "Welcome app": "ウェルカムアプリ", + "Disable tools": "ツールを無効化", + "Place at the bottom/right": "下/右に表示", + "EasyEffects | Right-click to configure": "EasyEffects | 右クリックで設定", + "Lap": "ラップ", + "**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**料金**: 制限付きの無料枠があります。詳細は https://docs.github.com/en/billing/concepts/product-billing/github-models を参照\n\n**手順**: GitHubの個人アクセストークンをModels権限で生成し、APIキーとして設定してください\n\n**注意**: 使用するには温度パラメータを1に設定する必要があります", + "Usage: %1save CHAT_NAME": "使用法: %1save チャット名", + "Approve": "承認", + "Current tool: %1\nSet it with %2tool TOOL": "現在のツール: %1\n%2tool ツール名 で設定できます", + "Depends on sidebars": "サイドバーに依存", + "Tint app icons": "アプリアイコンを色付け", + "Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls": "オンライン | %1のモデル | 高速で反応がよく、整形された回答を提供。短所: 積極性に欠ける場合あり。存在しない関数呼び出しを作成することも", + "Conflicts with the shell's system tray implementation": "シェルのシステムトレイ実装と競合します", + "There might be a download in progress": "ダウンロード処理が進行中の可能性があります", + "Overall appearance": "全体の見た目", + "🔴 Focus: %1 minutes": "🔴 集中: %1分", + "Tint icons": "アイコンを色付け", + "Thought": "思考プロセス", + "Bar layout": "バーのレイアウト", + "To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するには、%4コマンドで指定します\n\nキーを表示するには、コマンドに「get」を付けてください
\n\n### %1向け:\n\n**リンク**: %2\n\n%3", + "**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**手順**: Mistralアカウントにログインし、サイドバーの「Keys」に進み、「Create new key」をクリックしてください", + "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "オンライン | Googleのモデル\n前世代より処理は遅いですが、より高品質な回答を提供する新しいモデル", + "Enter password": "パスワードを入力", + "Keep right sidebar loaded": "右サイドバーを常にロードしておく", + "Kill conflicting programs?": "競合するプログラムを終了しますか?", + "Temperature\nChange with /temp VALUE": "温度\n/temp 値 で変更可能", + "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "オンライン | Googleのモデル\nコスト効率と高スループットに最適化されたGemini 2.5 Flashモデル。", + "🌿 Long break: %1 minutes": "🌿 長い休憩: %1分", + "Feels like %1": "体感温度 %1", + "Reject": "拒否", + "Preferred wallpaper zoom (%)": "壁紙の拡大率(%)", + "Attach a file. Only works with Gemini.": "ファイルを添付(Geminiのみ対応)", + "Your package manager is running": "パッケージマネージャーが実行中です", + "Invalid tool. Supported tools:\n- %1": "無効なツールです。対応ツール:\n- %1", + "☕ Break: %1 minutes": "☕ 休憩: %1分", + "Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "コマンド実行、設定編集、検索が可能。\n検索モードへの切替が必要な場合は追加の操作が必要です", + "Sidebars": "サイドバー", + "When enabled keeps the content of the right sidebar loaded to reduce the delay when opening,\nat the cost of around 15MB of consistent RAM usage. Delay significance depends on your system's performance.\nUsing a custom kernel like linux-cachyos might help": "有効にすると右サイドバーの内容を常にメモリに読み込み、開く際の遅延を\n軽減します。約15MBのメモリを常時使用します。遅延の体感はシステム性能に\n依存します。linux-cachyosなどのカスタムカーネルで改善できる場合もあります", + "Horizontal": "水平", + "Usage: %1load CHAT_NAME": "使用法: %1load チャット名", + "Set the tool to use for the model.": "モデルで使用するツールを設定します。", + "Start": "開始", + "No API key\nSet it with /key YOUR_API_KEY": "APIキーが設定されていません\n/key APIキー で設定できます", + "Gives the model search capabilities (immediately)": "モデルに検索能力を付与(即時)", + "Depends on workspace": "ワークスペース依存", + "Hi there! First things first...": "こんにちは!まずは最初のステップから...", + "Command rejected by user": "コマンドがユーザーにより拒否されました", + "Total token count\nInput: %1\nOutput: %2": "合計トークン数\n入力: %1\n出力: %2", + "Online | Google's model\nFast, can perform searches for up-to-date information": "オンライン | Googleのモデル\n高速で、最新情報の検索が可能", + "Reset": "リセット", + "Always": "常に", + "Timer": "タイマー", + "System uptime:": "システム稼働時間:", + "Conflicts with the shell's notification implementation": "シェルの通知機能実装と競合します", + "Corner style": "角のスタイル", + "Incorrect password": "パスワードが正しくありません", + "Night Light | Right-click to toggle Auto mode": "ナイトライト | 右クリックで自動モード切替", + "Connect": "接続", + "Region height": "領域の高さ", + "Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "プレーヤーがMPRIS対応か確認するか\n重複プレーヤーのフィルタリングをオフにしてみてください", + "No active player": "アクティブなプレーヤーがありません", + "Place at the bottom": "下部に配置", + "Language": "言語", + "Value scroll": "値のスクロール", + "Place the corners to trigger at the bottom": "トリガーとなるコーナーを下部に配置", + "Hit \"/\" to search": "「/」で検索", + "Place at bottom": "下部に配置", + "Swap:": "スワップ:", + "Not configured": "設定されていません", + "CPU usage": "CPU使用率", + "Hover to trigger": "マウスを近づけて起動", + "Swap usage": "スワップ使用状況", + "Search wallpapers": "壁紙を検索", + "Memory usage": "メモリ使用状況", + "Language setting saved. Please restart Quickshell (Ctrl+Super+R) to apply the new language.": "言語設定を保存しました。新しい言語を適用するにはQuickshellを再起動してください(Ctrl+Super+R)。", + "Visualize region": "領域を可視化", + "Connect to Wi-Fi": "Wi-Fiに接続", + "Details": "詳細", + "Select the language for the user interface.\n\"Auto\" will use your system's locale.": "インターフェイスの言語を選択してください。\n「自動」はシステムの言語設定を使用します。", + "Use the system file picker instead": "システムのファイル選択ダイアログを使用", + "Interface Language": "インターフェイス言語", + "Corner open": "コーナーで開く", + "Brightness and volume": "明るさと音量", + "Region width": "領域の幅", + "Password": "パスワード", + "When this is off you'll have to click": "オフの場合はクリックが必要です", + "Pick a wallpaper": "壁紙を選択", + "Click to toggle light/dark mode (applied when wallpaper is chosen)": "クリックでライト/ダークモードを切り替え(壁紙選択時に適用)", + "Auto (System)": "自動(システム)", + "Edit config": "設定を編集", + "Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "バーの位置に関わらず、画面の隅をクリックまたはマウスを\n近づけることでサイドバーを開けます", + "Edit directory": "ディレクトリを編集" } From 0905773d08c49f23f10575d3e26390644bc7b416 Mon Sep 17 00:00:00 2001 From: rain022 Date: Fri, 29 Aug 2025 05:43:18 +0900 Subject: [PATCH 46/68] I've made a slight adjustment to the nuance. It should be fine to merge now. --- .config/quickshell/translations/ja_JP.json | 348 ++++++++++----------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/.config/quickshell/translations/ja_JP.json b/.config/quickshell/translations/ja_JP.json index 7faf042a1..105bf6ecb 100644 --- a/.config/quickshell/translations/ja_JP.json +++ b/.config/quickshell/translations/ja_JP.json @@ -7,26 +7,26 @@ "Sa": "土/*keep*/", "Su": "日/*keep*/", "%1 characters": "%1 文字", - "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**料金**: 無料。データ利用ポリシーはアカウント設定によって異なります。\n\n**手順**: OpenRouterにログイン後、右上の「Keys」メニューから「Create API Key」をクリックしてください。", - "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**料金**: 無料。データは学習に使用されます。\n\n**手順**: Googleアカウントにログインし、AI StudioにGoogle Cloudプロジェクトなどの作成を許可した後、戻って「APIキーを取得」をクリックしてください。", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**料金**: 無料。データ利用ポリシーはOpenRouterアカウント設定によって異なります。\n\n**手順**: OpenRouterアカウントにログインし、右上メニューのKeysに進み、Create API Keyをクリックしてください。", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**料金**: 無料。データは学習に使用されます。\n\n**手順**: Googleアカウントにログインし、AI StudioにGoogle Cloudプロジェクトの作成などを許可し、戻ってAPIキーを取得してください。", ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": "Zerochanの注意:\n- 色を入力する必要があります\n- `sidebar.booru.zerochan.username`設定でユーザー名を設定してください。設定しないと[利用停止になる場合があります](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", - "No further instruction provided": "追加の指示がありません", + "No further instruction provided": "追加の指示はありません", "Action": "操作", "Add": "追加", "Add task": "タスクを追加", - "All-rounder | Good quality, decent quantity": "オールラウンド | 良質で十分な量", + "All-rounder | Good quality, decent quantity": "万能型 | 高品質・十分な量", "Allow NSFW": "NSFWを許可", "Allow NSFW content": "NSFWコンテンツを許可", "Anime": "アニメ", "Anime boorus": "アニメ画像掲示板", "App": "アプリ", - "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEscキーか画面をクリックでキャンセル", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEsc/クリックでキャンセル", "Bluetooth": "Bluetooth", - "Brightness": "画面の明るさ", + "Brightness": "明るさ", "Cancel": "キャンセル", "Cheat sheet": "チートシート", "Choose model": "モデルを選択", - "Clean stuff | Excellent quality, no NSFW": "健全コンテンツ | 最高品質、成人向けなし", + "Clean stuff | Excellent quality, no NSFW": "健全 | 高品質・NSFWなし", "Clear": "クリア", "Clear chat history": "チャット履歴を消去", "Clear the current list of images": "現在の画像リストをクリア", @@ -42,7 +42,7 @@ "Enter text to translate...": "翻訳するテキストを入力...", "Finished tasks will go here": "完了したタスクはここに表示されます", "For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 高品質", - "For storing API keys and other sensitive information": "APIキーなどの機密情報を安全に保管", + "For storing API keys and other sensitive information": "APIキーや機密情報の保存用", "Game mode": "ゲームモード", "Get the next page of results": "次のページを取得", "Hibernate": "休止", @@ -52,7 +52,7 @@ "Invalid arguments. Must provide `key` and `value`.": "無効な引数です。`key`と`value`を指定してください。", "Jump to current month": "現在の月へ移動", "Keep system awake": "システムをスリープさせない", - "Large images | God tier quality, no NSFW.": "大型画像 | 最上級の品質、成人向けなし", + "Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質・NSFWなし", "Large language models": "大規模言語モデル", "Launch": "起動", "Lock": "ロック", @@ -81,17 +81,17 @@ "Select Language": "言語を選択", "Session": "セッション", "Set API key": "APIキーを設定", - "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、その他のモデルは0~1の範囲で指定可能。デフォルトは0.5です。", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、他は0~1です。初期値は0.5です。", "Set the current API provider": "現在のAPIプロバイダーを設定します", "Shutdown": "シャットダウン", - "Silent": "サイレントモード", + "Silent": "サイレント", "Sleep": "スリープ", "System": "システム", "Task Manager": "タスクマネージャー", "Task description": "タスクの説明", "Temperature must be between 0 and 2": "温度は0~2の間で指定してください", - "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向け | 大量にあり多くがアダルト、品質は様々", - "The popular one | Best quantity, but quality can vary wildly": "人気コンテンツ | 最多の量だが品質は様々", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向け | 量は多いが品質は様々・NSFW多数", + "The popular one | Best quantity, but quality can vary wildly": "人気 | 量は最多だが品質は様々", "Thinking": "考え中", "Translation goes here...": "ここに翻訳が表示されます...", "Translator": "翻訳", @@ -102,16 +102,16 @@ "Unknown Title": "不明なタイトル", "View Markdown source": "Markdownソースを表示", "Volume": "音量", - "Volume mixer": "音量ミキサー", - "Waifus only | Excellent quality, limited quantity": "美少女キャラのみ | 高品質だが少量", + "Volume mixer": "ボリュームミキサー", + "Waifus only | Excellent quality, limited quantity": "キャラクター | 高品質・量は少なめ", "Waiting for response...": "応答待ち...", "Workspace": "ワークスペース", "Invalid API provider. Supported: \n-": "無効なAPIプロバイダー。対応: \n-", "Unknown command:": "不明なコマンド:", - "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/keyでオンラインモデルを開始\nCtrl+Oでサイドバーを開く\nCtrl+Pでサイドバーを別窓に", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/key でオンラインモデルを開始\nCtrl+O でサイドバーを展開\nCtrl+P でサイドバーをウィンドウ化", "Provider set to": "プロバイダーを設定しました:", "Invalid model. Supported: \n```": "無効なモデル。対応: \n```", - "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "検索に失敗しました。ヒント:\n- タグやNSFW設定を確認してください\n- 特定のタグがなければ、ページ番号を入力してみてください", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "うまくいきませんでした。ヒント:\n- タグやNSFW設定を確認\n- タグがなければページ番号を入力", "Switched to search mode. Continue with the user's request.": "検索モードに切り替えました。リクエストを続行します。", "Settings": "設定", "Save chat": "チャットを保存", @@ -124,57 +124,58 @@ "About": "このアプリについて", "Services": "サービス", "Style": "スタイル", + "Edit config": "設定を編集", "Colors & Wallpaper": "色と壁紙", "Light": "ライト", "Dark": "ダーク", - "Material palette": "カラーパレット", - "Fidelity": "精度", - "Fruit Salad": "フルーツカラー", - "Alternatively use /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgも利用可能", - "Fake screen rounding": "画面の角を疑似的に丸くする", - "When not fullscreen": "全画面表示以外のとき", + "Material palette": "マテリアルパレット", + "Fidelity": "忠実度", + "Fruit Salad": "フルーツサラダ", + "Alternatively use /dark, /light, /img in the launcher": "ランチャーで /dark, /light, /img も使用できます", + "Fake screen rounding": "画面の角を丸める(疑似)", + "When not fullscreen": "フルスクリーンでない時", "Choose file": "ファイルを選択", - "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Konachanから一般向けアニメ壁紙をランダム取得\n画像は~/Pictures/Wallpapersに保存", - "Be patient...": "しばらくお待ちください...", - "Decorations & Effects": "装飾とエフェクト", - "Tonal Spot": "色調スポット", - "Shell windows": "ウィンドウ装飾", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Konachanから健全なアニメ壁紙をランダムで取得し、~/Pictures/Wallpapers に保存します", + "Be patient...": "少々お待ちください…", + "Decorations & Effects": "装飾と効果", + "Tonal Spot": "トーナルスポット", + "Shell windows": "シェルウィンドウ", "Auto": "自動", "Wallpaper": "壁紙", "Content": "コンテンツ", "Title bar": "タイトルバー", "Transparency": "透明度", "Expressive": "表現豊か", - "Yes": "はい", + "Yes": "表示", "Enable": "有効化", "Rainbow": "レインボー", - "Might look ass. Unsupported.": "見た目が悪くなる可能性あり(非対応)", + "Might look ass. Unsupported.": "表示が崩れる可能性があります(非推奨)", "Monochrome": "モノクロ", "Random: Konachan": "ランダム: Konachan", "Center title": "タイトルを中央に", "Neutral": "ニュートラル", "Pick wallpaper image on your system": "システムから壁紙画像を選択", - "No": "いいえ", + "No": "非表示", "AI": "AI", "Local only": "ローカルのみ", "Policies": "ポリシー", - "Weeb": "アニメ・漫画趣味", - "Closet": "クローゼット", + "Weeb": "アニメファン向け", + "Closet": "隠し", "Show next time": "次回も表示する", "Usage": "使用状況", - "Plain rectangle": "通常の四角形", - "Useless buttons": "装飾ボタン", + "Plain rectangle": "長方形", + "Useless buttons": "ダミーボタン", "GitHub": "GitHub", "Style & wallpaper": "スタイルと壁紙", "Configuration": "設定", "Change any time later with /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgでいつでも変更可能", "Keybinds": "キー割り当て", - "Float": "浮かせる", - "Hug": "内側に寄せる", - "illogical-impulse Welcome": "illogical-impulseへようこそ", + "Float": "フローティング", + "Hug": "固定", + "illogical-impulse Welcome": "illogical-impulse へようこそ!", "Info": "情報", - "Volume limit": "音量制限", - "Prevents abrupt increments and restricts volume limit": "急激な音量上昇を防ぎ、音量を制限します", + "Volume limit": "ボリューム制限", + "Prevents abrupt increments and restricts volume limit": "急な音量変化を防ぎ、音量の上限を設定します", "Resources": "リソース", "12h am/pm": "12時間(AM/PM)", "Base URL": "ベースURL", @@ -185,31 +186,31 @@ "Battery": "バッテリー", "Prefixes": "接頭辞", "Emojis": "絵文字", - "Earbang protection": "急な音量上昇防止", - "Automatically suspends the system when battery is low": "バッテリー残量が少なくなると自動的にシステムをスリープします", + "Earbang protection": "聴覚保護(急な大音量防止)", + "Automatically suspends the system when battery is low": "バッテリー残量が少ないときに自動でスリープします", "Automatic suspend": "自動スリープ", "Suspend at": "スリープ開始残量(%)", - "Max allowed increase": "最大許容増加幅", + "Max allowed increase": "音量の最大増加幅", "Web search": "ウェブ検索", "Polling interval (ms)": "ポーリング間隔(ms)", "Clipboard": "クリップボード", "Low warning": "バッテリー低下の警告", "24h": "24時間", - "Use Levenshtein distance-based algorithm instead of fuzzy": "あいまい検索の代わりに文字間の類似度アルゴリズムを使用", + "Use Levenshtein distance-based algorithm instead of fuzzy": "あいまい検索の代わりにレーベンシュタイン距離アルゴリズムを使用", "System prompt": "システムプロンプト", "12h AM/PM": "12時間(AM/PM)", - "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "タイプミスが多い場合に役立ちますが、検索結果が不自然になることがあり、頭字語では機能しないことも\n(例:「GIMP」で画像編集ソフトが検索できないなど)", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)":"入力ミスが多い場合に便利ですが、結果が意図しないものになったり、略語(例:GIMP)が正しく検索できないことがあります", "Critical warning": "重大な警告", "User agent (for services that require it)": "ユーザーエージェント(必要なサービス用)", - "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらは画像や画面上の一定のまとまりを持つ部分です。\n必ずしも正確ではない場合があります。\nローカルの画像処理アルゴリズムを使用しており、AIは使用していません。", - "Note: turning off can hurt readability": "注意:オフにすると読みやすさが低下する場合があります", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらの領域は、画像や画面の一部など、まとまりのある部分を指します。\n必ずしも正確ではありません。\nAIではなく、ローカルで実行される画像処理アルゴリズムによって検出されます。", + "Note: turning off can hurt readability": "注意: オフにすると可読性が損なわれる場合があります", "Workspaces shown": "表示中のワークスペース", "Dark/Light toggle": "ダーク/ライト切替", "Dock": "ドック", "Weather": "天気", "Pinned on startup": "起動時にピン留め", - "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント:アイコンを非表示にして数字を常に表示すると\n従来のシンプルな操作感になります", - "Always show numbers": "常に数字を表示", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを非表示にして番号を常に表示すると、クラシックなillogical-impulseの使用感を体験できます", + "Always show numbers": "数字を常に表示", "Buttons": "ボタン", "Keyboard toggle": "キーボード切替", "Scale (%)": "スケール(%)", @@ -217,15 +218,15 @@ "Rows": "行数", "Borderless": "枠なし", "Screenshot tool": "スクリーンショットツール", - "Number show delay when pressing Super (ms)": "Superキー押下時の数字表示遅延時間(ミリ秒)", + "Number show delay when pressing Super (ms)": "Superキー押下時の数字表示遅延(ms)", "Timeout (ms)": "タイムアウト(ms)", "Show app icons": "アプリアイコンを表示", "Workspaces": "ワークスペース", "Columns": "列数", - "On-screen display": "オンスクリーン表示", - "Screen snip": "画面キャプチャ", + "On-screen display": "画面表示", + "Screen snip": "画面の切り抜き", "Mic toggle": "マイク切替", - "Hover to reveal": "マウスを近づけると表示", + "Hover to reveal": "ホバーで表示", "Bar": "バー", "Show background": "背景を表示", "Show regions of potential interest": "注目領域を表示", @@ -237,7 +238,7 @@ "Distro": "ディストリビューション", "Privacy Policy": "プライバシーポリシー", "Documentation": "ドキュメント", - "Shell & utilities theming must also be enabled": "シェルとユーティリティのテーマも有効にする必要があります", + "Shell & utilities theming must also be enabled": "「シェルとユーティリティ」のテーマ設定も有効にする必要があります", "illogical-impulse": "illogical-impulse", "Donate": "寄付", "Terminal": "ターミナル", @@ -247,7 +248,7 @@ "Issues": "課題", "Drag or click a region • LMB: Copy • RMB: Edit": "領域をドラッグまたはクリック • 左クリック: コピー • 右クリック: 編集", "Current model: %1\nSet it with %2model MODEL": "現在のモデル: %1\n%2model MODELで設定", - "Message the model... \"%1\" for commands": "モデルにメッセージ... コマンドは「%1」", + "Message the model... \"%1\" for commands": "モデルにメッセージを送信... コマンドは「%1」", "No API key set for %1": "%1のAPIキーが設定されていません", "Loaded the following system prompt\n\n---\n\n%1": "次のシステムプロンプトを読み込みました\n\n---\n\n%1", "%1 | Right-click to configure": "%1 | 右クリックで設定", @@ -276,19 +277,19 @@ "Critically low battery": "バッテリー残量が非常に少ない", "Select output device": "出力デバイスを選択", "Code saved to file": "コードをファイルに保存しました", - "Online models disallowed\n\nControlled by `policies.ai` config option": "オンラインモデルは許可されていません\n\n`policies.ai`設定で制御されています", + "Online models disallowed\n\nControlled by `policies.ai` config option": "オンラインモデルは許可されていません\n\n`policies.ai` 設定で管理されています", "Scroll to change volume": "スクロールで音量調整", "Elements": "要素", "%1 • %2 tasks": "%1 • %2件のタスク", "Download complete": "ダウンロード完了", - "Please charge!\nAutomatic suspend triggers at %1": "充電してください!\n%1で自動スリープが作動します", + "Please charge!\nAutomatic suspend triggers at %1": "充電してください!\n%1で自動的にサスペンドします", "Cloudflare WARP": "Cloudflare WARP", "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", "Scroll to change brightness": "スクロールで明るさ調整", - "Connection failed. Please inspect manually with the warp-cli command": "接続に失敗しました。warp-cliコマンドで手動確認してください", + "Connection failed. Please inspect manually with the warp-cli command": "接続に失敗しました。warp-cli コマンドで手動確認してください", "Select input device": "入力デバイスを選択", - "Registration failed. Please inspect manually with the warp-cli command": "登録に失敗しました。warp-cliコマンドで手動確認してください", - "Consider plugging in your device": "電源に接続してください", + "Registration failed. Please inspect manually with the warp-cli command": "登録に失敗しました。warp-cli コマンドで手動確認してください", + "Consider plugging in your device": "電源を接続することを推奨します", "Low battery": "バッテリー残量低下", "Saved to %1": "%1に保存しました", "Sunset": "日没", @@ -309,124 +310,123 @@ "Used:": "使用済み:", "Free:": "空き:", "Total:": "合計:", + "Swap:": "スワップ:", + "Not configured": "未設定", "Load:": "負荷:", "High": "高", "Medium": "中", "Low": "低", - "Shell conflicts killer": "シェル競合解決ツール", - "Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "オンライン | Googleのモデル\nコーディングや複雑な推論タスクに優れた、Googleの最先端多目的モデルです。", - "Pause": "一時停止", - "Vertical": "垂直", - "Tray": "トレイ", - "Tool set to: %1": "ツールを設定しました: %1", - "Stopwatch": "ストップウォッチ", - "Performance Profile toggle": "パフォーマンスプロファイル切替", - "Usage: %1tool TOOL_NAME": "使用法: %1tool ツール名", - "Focus": "集中モード", - "Pomodoro": "ポモドーロタイマー", - "API key is set\nChange with /key YOUR_API_KEY": "APIキーが設定されています\n/key APIキー で変更できます", - "Up %1": "稼働時間 %1", - "Config file": "設定ファイル", - "Automatically hide": "自動的に隠す", - "Refreshing (manually triggered)": "更新中(手動実行)", - "Resume": "再開", - "Enjoy! You can reopen the welcome app any time with Super+Shift+Alt+/. To open the settings app, hit Super+I": "ようこそ!ウェルカムアプリはSuper+Shift+Alt+/で再表示できます。設定アプリを開くにはSuper+Iを押してください", - "Long break": "長い休憩", - "To Do:": "やることリスト:", - "Invalid arguments. Must provide `command`.": "引数が無効です。`command`を指定してください。", - "Wallpaper parallax": "壁紙の視差効果", - "Break": "休憩", - "Open the shell config file.\nIf the button doesn't work or doesn't open in your favorite editor,\nyou can manually open ~/.config/illogical-impulse/config.json": "シェル設定ファイルを開きます。\nボタンが機能しないか、お好みのエディタで開かない場合は、\n手動で~/.config/illogical-impulse/config.jsonを開いてください", - "Welcome app": "ウェルカムアプリ", - "Disable tools": "ツールを無効化", - "Place at the bottom/right": "下/右に表示", - "EasyEffects | Right-click to configure": "EasyEffects | 右クリックで設定", - "Lap": "ラップ", - "**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**料金**: 制限付きの無料枠があります。詳細は https://docs.github.com/en/billing/concepts/product-billing/github-models を参照\n\n**手順**: GitHubの個人アクセストークンをModels権限で生成し、APIキーとして設定してください\n\n**注意**: 使用するには温度パラメータを1に設定する必要があります", - "Usage: %1save CHAT_NAME": "使用法: %1save チャット名", - "Approve": "承認", - "Current tool: %1\nSet it with %2tool TOOL": "現在のツール: %1\n%2tool ツール名 で設定できます", - "Depends on sidebars": "サイドバーに依存", - "Tint app icons": "アプリアイコンを色付け", - "Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls": "オンライン | %1のモデル | 高速で反応がよく、整形された回答を提供。短所: 積極性に欠ける場合あり。存在しない関数呼び出しを作成することも", - "Conflicts with the shell's system tray implementation": "シェルのシステムトレイ実装と競合します", - "There might be a download in progress": "ダウンロード処理が進行中の可能性があります", - "Overall appearance": "全体の見た目", - "🔴 Focus: %1 minutes": "🔴 集中: %1分", - "Tint icons": "アイコンを色付け", - "Thought": "思考プロセス", - "Bar layout": "バーのレイアウト", - "To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するには、%4コマンドで指定します\n\nキーを表示するには、コマンドに「get」を付けてください
\n\n### %1向け:\n\n**リンク**: %2\n\n%3", - "**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**手順**: Mistralアカウントにログインし、サイドバーの「Keys」に進み、「Create new key」をクリックしてください", - "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "オンライン | Googleのモデル\n前世代より処理は遅いですが、より高品質な回答を提供する新しいモデル", - "Enter password": "パスワードを入力", - "Keep right sidebar loaded": "右サイドバーを常にロードしておく", - "Kill conflicting programs?": "競合するプログラムを終了しますか?", - "Temperature\nChange with /temp VALUE": "温度\n/temp 値 で変更可能", - "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "オンライン | Googleのモデル\nコスト効率と高スループットに最適化されたGemini 2.5 Flashモデル。", - "🌿 Long break: %1 minutes": "🌿 長い休憩: %1分", - "Feels like %1": "体感温度 %1", - "Reject": "拒否", - "Preferred wallpaper zoom (%)": "壁紙の拡大率(%)", - "Attach a file. Only works with Gemini.": "ファイルを添付(Geminiのみ対応)", - "Your package manager is running": "パッケージマネージャーが実行中です", - "Invalid tool. Supported tools:\n- %1": "無効なツールです。対応ツール:\n- %1", - "☕ Break: %1 minutes": "☕ 休憩: %1分", - "Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "コマンド実行、設定編集、検索が可能。\n検索モードへの切替が必要な場合は追加の操作が必要です", - "Sidebars": "サイドバー", - "When enabled keeps the content of the right sidebar loaded to reduce the delay when opening,\nat the cost of around 15MB of consistent RAM usage. Delay significance depends on your system's performance.\nUsing a custom kernel like linux-cachyos might help": "有効にすると右サイドバーの内容を常にメモリに読み込み、開く際の遅延を\n軽減します。約15MBのメモリを常時使用します。遅延の体感はシステム性能に\n依存します。linux-cachyosなどのカスタムカーネルで改善できる場合もあります", - "Horizontal": "水平", - "Usage: %1load CHAT_NAME": "使用法: %1load チャット名", - "Set the tool to use for the model.": "モデルで使用するツールを設定します。", - "Start": "開始", - "No API key\nSet it with /key YOUR_API_KEY": "APIキーが設定されていません\n/key APIキー で設定できます", - "Gives the model search capabilities (immediately)": "モデルに検索能力を付与(即時)", - "Depends on workspace": "ワークスペース依存", - "Hi there! First things first...": "こんにちは!まずは最初のステップから...", - "Command rejected by user": "コマンドがユーザーにより拒否されました", - "Total token count\nInput: %1\nOutput: %2": "合計トークン数\n入力: %1\n出力: %2", - "Online | Google's model\nFast, can perform searches for up-to-date information": "オンライン | Googleのモデル\n高速で、最新情報の検索が可能", - "Reset": "リセット", - "Always": "常に", - "Timer": "タイマー", - "System uptime:": "システム稼働時間:", - "Conflicts with the shell's notification implementation": "シェルの通知機能実装と競合します", - "Corner style": "角のスタイル", - "Incorrect password": "パスワードが正しくありません", - "Night Light | Right-click to toggle Auto mode": "ナイトライト | 右クリックで自動モード切替", - "Connect": "接続", - "Region height": "領域の高さ", - "Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "プレーヤーがMPRIS対応か確認するか\n重複プレーヤーのフィルタリングをオフにしてみてください", - "No active player": "アクティブなプレーヤーがありません", - "Place at the bottom": "下部に配置", - "Language": "言語", - "Value scroll": "値のスクロール", - "Place the corners to trigger at the bottom": "トリガーとなるコーナーを下部に配置", - "Hit \"/\" to search": "「/」で検索", - "Place at bottom": "下部に配置", - "Swap:": "スワップ:", - "Not configured": "設定されていません", - "CPU usage": "CPU使用率", - "Hover to trigger": "マウスを近づけて起動", - "Swap usage": "スワップ使用状況", - "Search wallpapers": "壁紙を検索", - "Memory usage": "メモリ使用状況", - "Language setting saved. Please restart Quickshell (Ctrl+Super+R) to apply the new language.": "言語設定を保存しました。新しい言語を適用するにはQuickshellを再起動してください(Ctrl+Super+R)。", - "Visualize region": "領域を可視化", + "Use the system file picker instead": "システム標準のファイル選択ツールを使用", + "Tint icons": "アイコンに色付けする", "Connect to Wi-Fi": "Wi-Fiに接続", - "Details": "詳細", - "Select the language for the user interface.\n\"Auto\" will use your system's locale.": "インターフェイスの言語を選択してください。\n「自動」はシステムの言語設定を使用します。", - "Use the system file picker instead": "システムのファイル選択ダイアログを使用", - "Interface Language": "インターフェイス言語", - "Corner open": "コーナーで開く", - "Brightness and volume": "明るさと音量", - "Region width": "領域の幅", - "Password": "パスワード", - "When this is off you'll have to click": "オフの場合はクリックが必要です", - "Pick a wallpaper": "壁紙を選択", - "Click to toggle light/dark mode (applied when wallpaper is chosen)": "クリックでライト/ダークモードを切り替え(壁紙選択時に適用)", + "Invalid arguments. Must provide `command`.": "無効な引数です。`command`を指定してください。", + "System uptime:": "システム稼働時間:", + "Gives the model search capabilities (immediately)": "モデルにすぐに検索能力を与えます", + "Click to toggle light/dark mode (applied when wallpaper is chosen)": "クリックでライト/ダークモード切替(壁紙選択時に適用されます)", + "**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**料金**: 制限付きの無料枠があります。詳細は https://docs.github.com/en/billing/concepts/product-billing/github-models を参照\n\n**手順**: Modelsアクセス権を持つGitHub個人アクセストークンを生成し、ここでAPIキーとして設定してください\n\n**注意**: 使用するには温度パラメータを1に設定する必要があります", + "Depends on workspace": "ワークスペースに依存", + "Hi there! First things first...": "こんにちは!まずは始めましょう...", + "Refreshing (manually triggered)": "更新中(手動で開始)", + "Always": "常に", + "No API key\nSet it with /key YOUR_API_KEY": "APIキーがありません\n/key YOUR_API_KEYで設定してください", + "Usage: %1load CHAT_NAME": "使用法: %1load チャット名", + "Sidebars": "サイドバー", + "Search wallpapers": "壁紙を検索", + "When this is off you'll have to click": "オフの場合、クリックで表示します", + "Depends on sidebars": "サイドバーに依存", + "Incorrect password": "パスワードが間違っています", + "Current tool: %1\nSet it with %2tool TOOL": "現在のツール: %1\n%2tool TOOLで設定", + "Overall appearance": "全体の外観", + "To Do:": "やること:", + "Region height": "領域の高さ", "Auto (System)": "自動(システム)", - "Edit config": "設定を編集", - "Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "バーの位置に関わらず、画面の隅をクリックまたはマウスを\n近づけることでサイドバーを開けます", - "Edit directory": "ディレクトリを編集" + "Place the corners to trigger at the bottom": "トリガーとなる角を下部に配置", + "Shell conflicts killer": "シェルの競合解消", + "Enter password": "パスワードを入力", + "☕ Break: %1 minutes": "☕ 休憩: %1分", + "Reset": "リセット", + "Connect": "接続", + "Tint app icons": "アプリアイコンに淡い色付けをする", + "Bar layout": "バーのレイアウト", + "Conflicts with the shell's notification implementation": "シェルの通知機能と競合することがあります", + "Corner open": "コーナー起動", + "🌿 Long break: %1 minutes": "🌿 長い休憩: %1分", + "Reject": "拒否", + "Command rejected by user": "コマンドはユーザーによって拒否されました", + "Start": "開始", + "Brightness and volume": "明るさ・音量", + "Corner style": "角のスタイル", + "Total token count\nInput: %1\nOutput: %2": "トークン数合計\n入力: %1\n出力: %2", + "No active player": "アクティブなプレーヤーがありません", + "Performance Profile toggle": "パフォーマンスプロファイル切替", + "Timer": "タイマー", + "Conflicts with the shell's system tray implementation": "シェルのシステムトレイ機能と競合することがあります", + "Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls": "オンライン | %1のモデル | 高速で反応が良く、整形された回答が得られます。欠点: あまり積極的でない場合があり、存在しない関数を提案することがあります", + "Welcome app": "ようこそアプリ", + "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "オンライン | Googleモデル\nコスト効率と高スループットに最適化されたGemini 2.5 Flashモデル。", + "Wallpaper parallax": "壁紙パララックス効果", + "Place at the bottom": "下部に配置", + "Invalid tool. Supported tools:\n- %1": "無効なツールです。対応ツール:\n- %1", + "Password": "パスワード", + "Details": "詳細", + "Edit directory": "ディレクトリを編集", + "Language": "言語", + "Visualize region": "領域を可視化", + "Enjoy! You can reopen the welcome app any time with Super+Shift+Alt+/. To open the settings app, hit Super+I": "お楽しみください!ウェルカムアプリはSuper+Shift+Alt+/でいつでも開けます。設定アプリを開くにはSuper+Iを押してください", + "Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "オンライン | Googleモデル\nコーディングや複雑な推論タスクに優れた、Googleの最先端多目的モデル。", + "When enabled keeps the content of the right sidebar loaded to reduce the delay when opening,\nat the cost of around 15MB of consistent RAM usage. Delay significance depends on your system's performance.\nUsing a custom kernel like linux-cachyos might help": "有効にすると右サイドバーのコンテンツを常に読み込んでおくことで、表示遅延を短縮します。\n代償として約15MBのメモリを継続的に使用します。遅延の度合いはシステム性能に依存します。\nlinux-cachyosのようなカスタムカーネルの使用が効果的な場合があります。", + "Place at bottom": "下部に配置", + "Tool set to: %1": "ツールを設定: %1", + "Set the tool to use for the model.": "モデルが使用するツールを設定します。", + "Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "お使いのプレイヤーがMPRISをサポートしているか確認するか、\n重複プレイヤーのフィルタリングを無効にしてみてください", + "Select the language for the user interface.\n\"Auto\" will use your system's locale.": "インターフェース言語を選択します。\n「自動」を選ぶとシステムのロケールが使用されます。", + "Value scroll": "スクロールで音量・明るさを調整", + "There might be a download in progress": "ダウンロードが進行中のようです", + "Approve": "承認", + "Tray": "トレイ", + "**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**手順**: Mistralアカウントにログインし、サイドバーのKeysに進み、Create new keyをクリックしてください", + "Pomodoro": "ポモドーロ", + "Language setting saved. Please restart Quickshell (Ctrl+Super+R) to apply the new language.": "言語設定を保存しました。新しい言語を適用するにはQuickshellを再起動してください(Ctrl+Super+R)。", + "Feels like %1": "体感温度 %1", + "Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "コマンド、設定編集、検索が可能。\n検索モードへの切替が必要な場合は追加の対話が必要です", + "Resume": "再開", + "Preferred wallpaper zoom (%)": "壁紙の拡大率(%)", + "Disable tools": "ツールをオフ", + "Night Light | Right-click to toggle Auto mode": "ナイトライト | 右クリックで自動モード切替", + "Online | Google's model\nFast, can perform searches for up-to-date information": "オンライン | Googleモデル\n高速で、最新情報を検索できます", + "Hover to trigger": "ホバーでトリガー", + "Keep right sidebar loaded": "右サイドバーを常に読み込む", + "Kill conflicting programs?": "競合するプログラムを終了しますか?", + "Pick a wallpaper": "壁紙を選択", + "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "オンライン | Googleモデル\n旧モデルより低速ですが、より高品質な回答が期待できる新モデル", + "Hit \"/\" to search": "「/」で検索", + "Config file": "設定ファイル", + "Attach a file. Only works with Gemini.": "ファイルを添付 (Geminiでのみ利用可能)", + "To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するには、%4コマンドを使用してください\n\nキーを確認するには、コマンドに「get」を付けてください
\n\n### %1について:\n\n**リンク**: %2\n\n%3", + "Thought": "思考", + "Long break": "長い休憩", + "Temperature\nChange with /temp VALUE": "温度\n/temp 値で変更", + "Usage: %1tool TOOL_NAME": "使用法: %1tool ツール名", + "Pause": "一時停止", + "Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "バーの配置に関わらず、画面コーナーのクリックまたはホバーでサイドバーを開けるようにします", + "EasyEffects | Right-click to configure": "EasyEffects | 右クリックで設定", + "Up %1": "%1稼働中", + "Place at the bottom/right": "下部/右側に配置", + "Focus": "集中", + "Stopwatch": "ストップウォッチ", + "Interface Language": "インターフェース言語", + "Memory usage": "メモリ使用量", + "Automatically hide": "自動的に隠す", + "Break": "休憩", + "Your package manager is running": "パッケージマネージャーが実行中です", + "API key is set\nChange with /key YOUR_API_KEY": "APIキーが設定済み\n/key YOUR_API_KEYで変更できます", + "Open the shell config file.\nIf the button doesn't work or doesn't open in your favorite editor,\nyou can manually open ~/.config/illogical-impulse/config.json": "シェルの設定ファイルを開きます。\nボタンが機能しない、または任意のエディタで開かない場合は、\n~/.config/illogical-impulse/config.json を手動で開いてください", + "CPU usage": "CPU使用率", + "Swap usage": "スワップ使用量", + "Usage: %1save CHAT_NAME": "使用法: %1save チャット名", + "🔴 Focus: %1 minutes": "🔴 集中: %1分", + "Lap": "ラップ", + "Horizontal": "水平", + "Region width": "領域の幅", + "Vertical": "垂直" } From d239ab6b1f2a4303294188cc2e5d014500e31fad Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:06:01 +0700 Subject: [PATCH 47/68] bluetooth: refractor and fix wrong icon --- .../quickshell/ii/modules/bar/BarContent.qml | 6 +----- .../quickToggles/BluetoothToggle.qml | 13 ++++++------- .../verticalBar/VerticalBarContent.qml | 5 +---- .../ii/services/BluetoothStatus.qml | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 .config/quickshell/ii/services/BluetoothStatus.qml diff --git a/.config/quickshell/ii/modules/bar/BarContent.qml b/.config/quickshell/ii/modules/bar/BarContent.qml index d9670b642..45656992c 100644 --- a/.config/quickshell/ii/modules/bar/BarContent.qml +++ b/.config/quickshell/ii/modules/bar/BarContent.qml @@ -2,7 +2,6 @@ import "./weather" import QtQuick import QtQuick.Layouts import Quickshell -import Quickshell.Bluetooth import Quickshell.Services.UPower import qs import qs.services @@ -307,10 +306,7 @@ Item { // Bar content region color: rightSidebarButton.colText } MaterialSymbol { - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) - readonly property bool bluetoothConnected: bluetoothDevice !== undefined - text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" + text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled" iconSize: Appearance.font.pixelSize.larger color: rightSidebarButton.colText } diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml index d1ad1c3a6..1a4c053ed 100644 --- a/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml @@ -1,4 +1,5 @@ import qs +import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions @@ -10,11 +11,8 @@ import Quickshell.Hyprland QuickToggleButton { id: root - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled ?? false - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null - readonly property bool bluetoothConnected: bluetoothDevice !== undefined - toggled: bluetoothEnabled - buttonIcon: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" + toggled: BluetoothStatus.enabled + buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled" onClicked: { Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled } @@ -24,7 +22,8 @@ QuickToggleButton { } StyledToolTip { content: Translation.tr("%1 | Right-click to configure").arg( - (bluetoothDevice?.name.length > 0) ? - bluetoothDevice.name : Translation.tr("Bluetooth")) + (BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth")) + + (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "") + ) } } diff --git a/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml b/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml index 66ccd78f4..a69625de3 100644 --- a/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml +++ b/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml @@ -285,10 +285,7 @@ Item { // Bar content region color: rightSidebarButton.colText } MaterialSymbol { - readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter?.enabled - readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) - readonly property bool bluetoothConnected: bluetoothDevice !== undefined - text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" + text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled" iconSize: Appearance.font.pixelSize.larger color: rightSidebarButton.colText } diff --git a/.config/quickshell/ii/services/BluetoothStatus.qml b/.config/quickshell/ii/services/BluetoothStatus.qml new file mode 100644 index 000000000..e2d3cee56 --- /dev/null +++ b/.config/quickshell/ii/services/BluetoothStatus.qml @@ -0,0 +1,19 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Io +import QtQuick + +/** + * Network service with nmcli. + */ +Singleton { + id: root + + readonly property bool enabled: Bluetooth.defaultAdapter?.enabled + readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null + readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0 + readonly property bool connected: Bluetooth.devices.values.some(d => d.connected) +} From 41e1e8969664e2f5860705c891c512bebb06dbea Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 29 Aug 2025 07:29:14 +0200 Subject: [PATCH 48/68] sidebar: wifi menu: public wifi login page button --- .../wifiNetworks/WifiNetworkItem.qml | 40 +++++++++++++------ .config/quickshell/ii/services/Network.qml | 4 ++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index 733c04ef1..6afeab875 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -23,11 +23,11 @@ RippleButton { buttonRadius: 0 colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) - colBackgroundHover: wifiNetwork?.askingPassword ? colBackground : Appearance.colors.colLayer3Hover + colBackgroundHover: (wifiNetwork?.askingPassword || wifiNetwork?.active) ? colBackground : Appearance.colors.colLayer3Hover colRipple: Appearance.colors.colLayer3Active onClicked: { - Network.connectToWifiNetwork(wifiNetwork) + Network.connectToWifiNetwork(wifiNetwork); } contentItem: ColumnLayout { @@ -42,15 +42,12 @@ RippleButton { spacing: 0 RowLayout { + // Name spacing: 10 MaterialSymbol { iconSize: Appearance.font.pixelSize.larger property int strength: root.wifiNetwork?.strength ?? 0 - text: strength > 80 ? "signal_wifi_4_bar" : - strength > 60 ? "network_wifi_3_bar" : - strength > 40 ? "network_wifi_2_bar" : - strength > 20 ? "network_wifi_1_bar" : - "signal_wifi_0_bar" + text: strength > 80 ? "signal_wifi_4_bar" : strength > 60 ? "network_wifi_3_bar" : strength > 40 ? "network_wifi_2_bar" : strength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar" color: Appearance.colors.colOnSurfaceVariant } StyledText { @@ -66,10 +63,10 @@ RippleButton { } } - ColumnLayout { + ColumnLayout { // Password id: passwordPrompt visible: root.wifiNetwork?.askingPassword ?? false - Layout.topMargin: 12 + Layout.topMargin: 8 MaterialTextField { id: passwordField @@ -81,7 +78,7 @@ RippleButton { inputMethodHints: Qt.ImhSensitiveData onAccepted: { - Network.changePassword(root.wifiNetwork, passwordField.text) + Network.changePassword(root.wifiNetwork, passwordField.text); } } @@ -95,18 +92,37 @@ RippleButton { DialogButton { buttonText: Translation.tr("Cancel") onClicked: { - root.wifiNetwork.askingPassword = false + root.wifiNetwork.askingPassword = false; } } DialogButton { buttonText: Translation.tr("Connect") onClicked: { - Network.changePassword(root.wifiNetwork, passwordField.text) + Network.changePassword(root.wifiNetwork, passwordField.text); } } } + } + ColumnLayout { // Public wifi login page + id: publicWifiPortal + visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 + Layout.topMargin: 8 + + RowLayout { + DialogButton { + Layout.fillWidth: true + buttonText: Translation.tr("Open network portal") + colBackground: Appearance.colors.colLayer4 + colBackgroundHover: Appearance.colors.colLayer4Hover + colRipple: Appearance.colors.colLayer4Active + onClicked: { + Network.openPublicWifiPortal() + GlobalStates.sidebarRightOpen = false + } + } + } } } } diff --git a/.config/quickshell/ii/services/Network.qml b/.config/quickshell/ii/services/Network.qml index bb6e36f9c..ebca2a61d 100644 --- a/.config/quickshell/ii/services/Network.qml +++ b/.config/quickshell/ii/services/Network.qml @@ -62,6 +62,10 @@ Singleton { if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]); } + function openPublicWifiPortal() { + Quickshell.execDetached(["xdg-open", "https://nmcheck.gnome.org/"]) // From some StackExchange thread, seems to work + } + function changePassword(network: WifiAccessPoint, password: string, username = ""): void { // TODO: enterprise wifi with username network.askingPassword = false; From ba7ed5be1ce8ea15dd7ac1f39d8753a1000981d5 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:03:01 +0200 Subject: [PATCH 49/68] background: fix quote not working --- .config/quickshell/ii/modules/background/Background.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index 5df366d93..dca01f69d 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -278,7 +278,7 @@ Variants { style: Text.Raised visible: Config.options.background.mantra !== "" styleColor: Appearance.colors.colShadow - text: Config.options.background.mantra + text: Config.options.background.quote } } From 45894c32558ce4d1bbe7cde227f07b6759563e76 Mon Sep 17 00:00:00 2001 From: kirisaki-vk Date: Fri, 29 Aug 2025 09:51:08 +0300 Subject: [PATCH 50/68] chore: use var instead of list --- .config/quickshell/ii/modules/common/Config.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index c8a8bc78a..8ce2d0c06 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -174,7 +174,10 @@ Singleton { property bool showAppIcons: true property bool alwaysShowNumbers: false property int showNumberDelay: 300 // milliseconds - property list numberMapping: [] + property var numberMapping: { + "1": "1", + "2": "2" + } } property JsonObject weather: JsonObject { property bool enable: false From a1479a9b6c8c8e3ce9a8672ec466071615c38bd1 Mon Sep 17 00:00:00 2001 From: Moeta Yuko Date: Fri, 29 Aug 2025 15:15:25 +0800 Subject: [PATCH 51/68] lock: use default pam config and start without password Fixes #1800 --- .config/quickshell/ii/modules/lock/LockContext.qml | 8 -------- .config/quickshell/ii/modules/lock/LockSurface.qml | 2 +- .config/quickshell/ii/modules/lock/pam/password.conf | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 .config/quickshell/ii/modules/lock/pam/password.conf diff --git a/.config/quickshell/ii/modules/lock/LockContext.qml b/.config/quickshell/ii/modules/lock/LockContext.qml index 18728136d..1aeefdc5a 100644 --- a/.config/quickshell/ii/modules/lock/LockContext.qml +++ b/.config/quickshell/ii/modules/lock/LockContext.qml @@ -33,8 +33,6 @@ Scope { } function tryUnlock() { - if (currentText === "") return; - root.unlockInProgress = true; pam.start(); } @@ -42,12 +40,6 @@ Scope { PamContext { id: pam - // Its best to have a custom pam config for quickshell, as the system one - // might not be what your interface expects, and break in some way. - // This particular example only supports passwords. - configDirectory: "pam" - config: "password.conf" - // pam_unix will ask for a response for the password prompt onPamMessage: { if (this.responseRequired) { diff --git a/.config/quickshell/ii/modules/lock/LockSurface.qml b/.config/quickshell/ii/modules/lock/LockSurface.qml index a8ad410b8..87c5adb09 100644 --- a/.config/quickshell/ii/modules/lock/LockSurface.qml +++ b/.config/quickshell/ii/modules/lock/LockSurface.qml @@ -133,7 +133,7 @@ MouseArea { id: confirmButton implicitWidth: height toggled: true - enabled: !root.context.unlockInProgress && root.context.currentText.length > 0 + enabled: !root.context.unlockInProgress colBackgroundToggled: Appearance.colors.colPrimary onClicked: root.context.tryUnlock() diff --git a/.config/quickshell/ii/modules/lock/pam/password.conf b/.config/quickshell/ii/modules/lock/pam/password.conf deleted file mode 100644 index 7e5d75ae4..000000000 --- a/.config/quickshell/ii/modules/lock/pam/password.conf +++ /dev/null @@ -1 +0,0 @@ -auth required pam_unix.so From 59abffb1c178e09dea4f01e782a59c41506d6167 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 07:48:21 +0200 Subject: [PATCH 52/68] background: fix empty space between clock and lock text --- .config/quickshell/ii/modules/background/Background.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index dca01f69d..9caf90e91 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -276,7 +276,7 @@ Variants { } color: bgRoot.colText style: Text.Raised - visible: Config.options.background.mantra !== "" + visible: Config.options.background.quote !== "" styleColor: Appearance.colors.colShadow text: Config.options.background.quote } From a952ea02dc79e5ac557a9af2367cf8d1b8e9e021 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 07:51:18 +0200 Subject: [PATCH 53/68] bluetooth menu --- .../quickshell/ii/modules/common/Icons.qml | 23 ++++ .../modules/common/widgets/DialogButton.qml | 1 + .../modules/common/widgets/DialogListItem.qml | 25 ++++ .../modules/common/widgets/RippleButton.qml | 3 +- .../sidebarRight/SidebarRightContent.qml | 42 +++++-- .../bluetoothDevices/BluetoothDeviceItem.qml | 112 ++++++++++++++++++ .../bluetoothDevices/BluetoothDialog.qml | 67 +++++++++++ .../sidebarRight/wifiNetworks/WifiDialog.qml | 9 -- .../wifiNetworks/WifiNetworkItem.qml | 31 ++--- .../ii/services/BluetoothStatus.qml | 2 +- 10 files changed, 276 insertions(+), 39 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/Icons.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/DialogListItem.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml diff --git a/.config/quickshell/ii/modules/common/Icons.qml b/.config/quickshell/ii/modules/common/Icons.qml new file mode 100644 index 000000000..454aea11e --- /dev/null +++ b/.config/quickshell/ii/modules/common/Icons.qml @@ -0,0 +1,23 @@ +pragma Singleton + +// From https://github.com/caelestia-dots/shell (GPLv3) + +import Quickshell + +Singleton { + id: root + + function getBluetoothDeviceMaterialSymbol(systemIconName: string): string { + if (systemIconName.includes("headset") || systemIconName.includes("headphones")) + return "headphones"; + if (systemIconName.includes("audio")) + return "speaker"; + if (systemIconName.includes("phone")) + return "smartphone"; + if (systemIconName.includes("mouse")) + return "mouse"; + if (systemIconName.includes("keyboard")) + return "keyboard"; + return "bluetooth"; + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml index ba00d9b3c..9373a8cd5 100644 --- a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml @@ -20,6 +20,7 @@ RippleButton { colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) colBackgroundHover: Appearance.colors.colLayer3Hover colRipple: Appearance.colors.colLayer3Active + property alias colText: buttonTextWidget.color contentItem: StyledText { id: buttonTextWidget diff --git a/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml b/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml new file mode 100644 index 000000000..67205ced7 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/DialogListItem.qml @@ -0,0 +1,25 @@ +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import QtQuick + +RippleButton { + id: root + property bool active: false + + horizontalPadding: Appearance.rounding.large + verticalPadding: 12 + + clip: true + pointingHandCursor: !active + implicitWidth: contentItem.implicitWidth + horizontalPadding * 2 + implicitHeight: contentItem.implicitHeight + verticalPadding * 2 + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) + colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover + colRipple: Appearance.colors.colLayer3Active + buttonRadius: 0 +} diff --git a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml index b90c5e291..07e6c5318 100644 --- a/.config/quickshell/ii/modules/common/widgets/RippleButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml @@ -12,6 +12,7 @@ Button { id: root property bool toggled property string buttonText + property bool pointingHandCursor: true property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadiusPressed: buttonRadius property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius @@ -58,7 +59,7 @@ Button { MouseArea { anchors.fill: parent - cursorShape: Qt.PointingHandCursor + cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: (event) => { if(event.button === Qt.RightButton) { diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 37878a669..b1adc3ae6 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -1,18 +1,15 @@ import qs import qs.services -import qs.services.network import qs.modules.common import qs.modules.common.widgets -import qs.modules.common.functions import "./quickToggles/" import "./wifiNetworks/" +import "./bluetoothDevices/" import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Wayland +import Quickshell.Bluetooth import Quickshell.Hyprland Item { @@ -21,12 +18,14 @@ Item { property int sidebarPadding: 12 property string settingsQmlPath: Quickshell.shellPath("settings.qml") property bool showWifiDialog: false + property bool showBluetoothDialog: false Connections { target: GlobalStates function onSidebarRightOpenChanged() { if (!GlobalStates.sidebarRightOpen) { root.showWifiDialog = false; + root.showBluetoothDialog = false; } } } @@ -129,7 +128,13 @@ Item { root.showWifiDialog = true; } } - BluetoothToggle {} + BluetoothToggle { + altAction: () => { + Bluetooth.defaultAdapter.enabled = true; + Bluetooth.defaultAdapter.discovering = true; + root.showBluetoothDialog = true; + } + } NightLight {} GameMode {} IdleInhibitor {} @@ -138,7 +143,6 @@ Item { } CenterWidgetGroup { - focus: sidebarRoot.visible Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.fillWidth: true @@ -176,4 +180,28 @@ Item { } } } + + onShowBluetoothDialogChanged: if (showBluetoothDialog) bluetoothDialogLoader.active = true; + Loader { + id: bluetoothDialogLoader + anchors.fill: parent + + active: root.showBluetoothDialog || item.visible + onActiveChanged: { + if (active) { + item.show = true; + item.forceActiveFocus(); + } + } + + sourceComponent: BluetoothDialog { + onDismiss: { + show = false + root.showBluetoothDialog = false + } + onVisibleChanged: { + if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false; + } + } + } } diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml new file mode 100644 index 000000000..19ee07234 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -0,0 +1,112 @@ +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Layouts + +DialogListItem { + id: root + required property var device + property bool expanded: false + pointingHandCursor: !expanded + + onClicked: expanded = !expanded + + component ActionButton: DialogButton { + colBackground: Appearance.colors.colPrimary + colBackgroundHover: Appearance.colors.colPrimaryHover + colRipple: Appearance.colors.colPrimaryActive + colText: Appearance.colors.colOnPrimary + } + + contentItem: ColumnLayout { + anchors { + fill: parent + topMargin: root.verticalPadding + leftMargin: root.horizontalPadding + rightMargin: root.horizontalPadding + } + spacing: 0 + + RowLayout { + // Name + spacing: 10 + + MaterialSymbol { + iconSize: Appearance.font.pixelSize.larger + text: Icons.getBluetoothDeviceMaterialSymbol(root.device?.icon || "") + color: Appearance.colors.colOnSurfaceVariant + } + + ColumnLayout { + spacing: 2 + Layout.fillWidth: true + StyledText { + Layout.fillWidth: true + color: Appearance.colors.colOnSurfaceVariant + elide: Text.ElideRight + text: root.device?.name + } + StyledText { + visible: root.device?.connected || root.device?.paired + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colSubtext + elide: Text.ElideRight + text: { + if (!root.device?.paired) return ""; + let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired"); + if (!root.device?.batteryAvailable) return statusText; + statusText += ` • ${root.device?.battery * 100}%`; + return statusText; + } + } + } + + MaterialSymbol { + text: "keyboard_arrow_down" + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnLayer3 + rotation: root.expanded ? 180 : 0 + Behavior on rotation { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + + RowLayout { + visible: root.expanded + Layout.topMargin: 8 + Item { + Layout.fillWidth: true + } + ActionButton { + buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect") + + onClicked: { + if (root.device?.connected) { + root.device.disconnect(); + } else { + root.device.connect(); + } + } + } + ActionButton { + visible: root.device?.paired + colBackground: Appearance.colors.colError + colBackgroundHover: Appearance.colors.colErrorHover + colRipple: Appearance.colors.colErrorActive + colText: Appearance.colors.colOnError + + buttonText: Translation.tr("Forget") + onClicked: { + root.device?.forget(); + } + } + } + Item { + Layout.fillHeight: true + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml new file mode 100644 index 000000000..4e5d99921 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml @@ -0,0 +1,67 @@ +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.Io +import Quickshell.Bluetooth +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland + +WindowDialog { + id: root + + WindowDialogTitle { + text: Translation.tr("Bluetooth devices") + } + // TODO: add indeterminate progress bar when scanning + WindowDialogSeparator {} + StyledListView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -15 + Layout.bottomMargin: -16 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + + clip: true + spacing: 0 + animateAppearance: false + + model: ScriptModel { + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) + } + delegate: BluetoothDeviceItem { + required property BluetoothDevice modelData + device: modelData + anchors { + left: parent?.left + right: parent?.right + } + } + } + WindowDialogSeparator {} + WindowDialogButtonRow { + DialogButton { + buttonText: Translation.tr("Details") + onClicked: { + Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]); + GlobalStates.sidebarRightOpen = false; + } + } + + Item { + Layout.fillWidth: true + } + + DialogButton { + buttonText: Translation.tr("Done") + onClicked: root.dismiss() + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml index 6569704a8..abd854cfc 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml @@ -3,17 +3,9 @@ import qs.services import qs.services.network import qs.modules.common import qs.modules.common.widgets -import qs.modules.common.functions -import "./quickToggles/" -import "./wifiNetworks/" import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland WindowDialog { id: root @@ -44,7 +36,6 @@ WindowDialog { return b.strength - a.strength; }) } - // model: Network.wifiNetworks delegate: WifiNetworkItem { required property WifiAccessPoint modelData wifiNetwork: modelData diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index 6afeab875..ac178fbe2 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -1,37 +1,21 @@ import qs import qs.modules.common -import qs.modules.common.functions import qs.modules.common.widgets import qs.services import qs.services.network import QtQuick import QtQuick.Layouts -import Quickshell -RippleButton { +DialogListItem { id: root required property WifiAccessPoint wifiNetwork - horizontalPadding: Appearance.rounding.large - verticalPadding: 12 - implicitWidth: mainLayout.implicitWidth + horizontalPadding * 2 - implicitHeight: mainLayout.implicitHeight + verticalPadding * 2 - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - clip: true - - buttonRadius: 0 - colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3) - colBackgroundHover: (wifiNetwork?.askingPassword || wifiNetwork?.active) ? colBackground : Appearance.colors.colLayer3Hover - colRipple: Appearance.colors.colLayer3Active - + active: (wifiNetwork?.askingPassword || wifiNetwork?.active) onClicked: { Network.connectToWifiNetwork(wifiNetwork); } contentItem: ColumnLayout { - id: mainLayout anchors { fill: parent topMargin: root.verticalPadding @@ -52,8 +36,9 @@ RippleButton { } StyledText { Layout.fillWidth: true - text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") color: Appearance.colors.colOnSurfaceVariant + elide: Text.ElideRight + text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") } MaterialSymbol { visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false @@ -65,8 +50,8 @@ RippleButton { ColumnLayout { // Password id: passwordPrompt - visible: root.wifiNetwork?.askingPassword ?? false Layout.topMargin: 8 + visible: root.wifiNetwork?.askingPassword ?? false MaterialTextField { id: passwordField @@ -107,8 +92,8 @@ RippleButton { ColumnLayout { // Public wifi login page id: publicWifiPortal - visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 Layout.topMargin: 8 + visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 RowLayout { DialogButton { @@ -124,5 +109,9 @@ RippleButton { } } } + + Item { + Layout.fillHeight: true + } } } diff --git a/.config/quickshell/ii/services/BluetoothStatus.qml b/.config/quickshell/ii/services/BluetoothStatus.qml index e2d3cee56..73978d634 100644 --- a/.config/quickshell/ii/services/BluetoothStatus.qml +++ b/.config/quickshell/ii/services/BluetoothStatus.qml @@ -12,7 +12,7 @@ import QtQuick Singleton { id: root - readonly property bool enabled: Bluetooth.defaultAdapter?.enabled + readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0 readonly property bool connected: Bluetooth.devices.values.some(d => d.connected) From 79f078653a9032c9d09e1eb2248c2294cc15c819 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 07:55:29 +0200 Subject: [PATCH 54/68] bluetooth menu: stop scanning when closed, fix null warnings --- .../ii/modules/sidebarRight/SidebarRightContent.qml | 5 ++++- .../sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index b1adc3ae6..636d57729 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -181,7 +181,10 @@ Item { } } - onShowBluetoothDialogChanged: if (showBluetoothDialog) bluetoothDialogLoader.active = true; + onShowBluetoothDialogChanged: { + if (showBluetoothDialog) bluetoothDialogLoader.active = true; + else Bluetooth.defaultAdapter.discovering = false; + } Loader { id: bluetoothDialogLoader anchors.fill: parent diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml index 19ee07234..fec9c4eb3 100644 --- a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -46,10 +46,10 @@ DialogListItem { Layout.fillWidth: true color: Appearance.colors.colOnSurfaceVariant elide: Text.ElideRight - text: root.device?.name + text: root.device?.name || Translation.tr("Unknown device") } StyledText { - visible: root.device?.connected || root.device?.paired + visible: (root.device?.connected || root.device?.paired) ?? false Layout.fillWidth: true font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colSubtext @@ -93,7 +93,7 @@ DialogListItem { } } ActionButton { - visible: root.device?.paired + visible: root.device?.paired ?? false colBackground: Appearance.colors.colError colBackgroundHover: Appearance.colors.colErrorHover colRipple: Appearance.colors.colErrorActive From fd3455d3ecb08b49bc4ce8b32ad8aa60e9181c72 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 09:09:05 +0200 Subject: [PATCH 55/68] wallpaper selector use Toolbar for name filter --- .../ii/modules/common/widgets/Toolbar.qml | 2 +- .../modules/common/widgets/WindowDialog.qml | 2 +- .../WallpaperSelectorContent.qml | 187 +++++++----------- 3 files changed, 77 insertions(+), 114 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/Toolbar.qml b/.config/quickshell/ii/modules/common/widgets/Toolbar.qml index 6826802ab..25a2665c4 100644 --- a/.config/quickshell/ii/modules/common/widgets/Toolbar.qml +++ b/.config/quickshell/ii/modules/common/widgets/Toolbar.qml @@ -23,7 +23,7 @@ Item { Rectangle { id: background anchors.centerIn: parent - color: Appearance.colors.colLayer2 + color: Appearance.m3colors.m3surfaceContainer // Needs to be opaque implicitHeight: toolbarLayout.implicitHeight + root.padding * 2 implicitWidth: toolbarLayout.implicitWidth + root.padding * 2 radius: Appearance.rounding.full diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml index d4f9f3285..92f6353e3 100644 --- a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml @@ -45,7 +45,7 @@ Rectangle { id: dialogBackground anchors.horizontalCenter: parent.horizontalCenter radius: Appearance.rounding.large - color: Appearance.colors.colLayer3 + color: Appearance.m3colors.m3surfaceContainerHigh // Use opaque version of layer3 property real targetY: root.height / 2 - root.backgroundHeight / 2 y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index b55233417..0160f47c4 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -191,13 +191,16 @@ 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 + + StyledProgressBar { + anchors { + bottom: parent.top + left: parent.left + right: parent.right + leftMargin: 4 + rightMargin: 4 } + indeterminate: true } GridView { @@ -253,127 +256,87 @@ Item { filterField.text = ""; } } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: gridDisplayRegion.width + height: gridDisplayRegion.height + radius: wallpaperGridBackground.radius + } + } } - Item { + Toolbar { 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 + bottomMargin: 8 } - Rectangle { // Bottom toolbar - id: extraOptionsBackground - property real padding: 6 - anchors { - fill: parent - margins: 8 + ToolbarButton { + implicitWidth: height + onClicked: { + Wallpapers.openFallbackPicker(root.useDarkMode); + GlobalStates.wallpaperSelectorOpen = false; } - color: Appearance.colors.colLayer2 - implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 - implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 - radius: Appearance.rounding.full + contentItem: MaterialSymbol { + text: "open_in_new" + iconSize: Appearance.font.pixelSize.larger + } + StyledToolTip { + content: Translation.tr("Use the system file picker instead") + } + } - RowLayout { - id: extraOptionsRowLayout - anchors { - fill: parent - margins: extraOptionsBackground.padding - } + ToolbarButton { + implicitWidth: height + 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)") + } + } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: height - buttonRadius: Appearance.rounding.full - onClicked: { - Wallpapers.openFallbackPicker(root.useDarkMode); - GlobalStates.wallpaperSelectorOpen = false; + ToolbarTextField { + id: filterField + placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password") + + // Style + clip: true + font.pixelSize: Appearance.font.pixelSize.small + + // Search + 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; } - contentItem: MaterialSymbol { - text: "open_in_new" - iconSize: Appearance.font.pixelSize.larger - } - StyledToolTip { - content: Translation.tr("Use the system file picker instead") + 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 - 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" - } - } + ToolbarButton { + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + } + contentItem: StyledText { + text: "Cancel" } } } From 3290755fa8403e1d62d68e05bee44e485412ce04 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 09:28:11 +0200 Subject: [PATCH 56/68] wallpaper selector: correct search box placeholder --- .../ii/modules/wallpaperSelector/WallpaperSelectorContent.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 0160f47c4..45a08e5c8 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -304,7 +304,7 @@ Item { ToolbarTextField { id: filterField - placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password") + placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search") // Style clip: true From d759bc274f10c6385b007b1bf77bd64fac9da465 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:34:32 +0200 Subject: [PATCH 57/68] bluetooth menu: right click to expand item --- .../sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml index fec9c4eb3..405f41c67 100644 --- a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -12,6 +12,7 @@ DialogListItem { pointingHandCursor: !expanded onClicked: expanded = !expanded + altAction: () => expanded = !expanded component ActionButton: DialogButton { colBackground: Appearance.colors.colPrimary From e5db36e21ea7b19657bd2f787550a07f72789f57 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:59:11 +0200 Subject: [PATCH 58/68] workspace number mapping: rename config var, use list --- .config/quickshell/ii/modules/bar/Workspaces.qml | 2 +- .config/quickshell/ii/modules/common/Config.qml | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/bar/Workspaces.qml b/.config/quickshell/ii/modules/bar/Workspaces.qml index d3ded59b8..74e0524d9 100644 --- a/.config/quickshell/ii/modules/bar/Workspaces.qml +++ b/.config/quickshell/ii/modules/bar/Workspaces.qml @@ -217,7 +217,7 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) - text: `${Config.options?.bar.workspaces.numberMapping[button.workspaceValue - 1] || button.workspaceValue}` + text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue elide: Text.ElideRight color: (monitor?.activeWorkspace?.id == button.workspaceValue) ? Appearance.m3colors.m3onPrimary : diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 8ce2d0c06..05627416a 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -174,10 +174,7 @@ Singleton { property bool showAppIcons: true property bool alwaysShowNumbers: false property int showNumberDelay: 300 // milliseconds - property var numberMapping: { - "1": "1", - "2": "2" - } + property list numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator } property JsonObject weather: JsonObject { property bool enable: false From 6a1acc819b5a2f3d24618b86f686b8c640206e8e Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:01:59 +0200 Subject: [PATCH 59/68] workspace numbers: allow using nerd font --- .config/quickshell/ii/modules/bar/Workspaces.qml | 5 ++++- .config/quickshell/ii/modules/common/Config.qml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/bar/Workspaces.qml b/.config/quickshell/ii/modules/bar/Workspaces.qml index 74e0524d9..a426ce71b 100644 --- a/.config/quickshell/ii/modules/bar/Workspaces.qml +++ b/.config/quickshell/ii/modules/bar/Workspaces.qml @@ -216,7 +216,10 @@ Item { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) + font { + pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) + family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : Appearance.font.family.main + } text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue elide: Text.ElideRight color: (monitor?.activeWorkspace?.id == button.workspaceValue) ? diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 05627416a..18172a02f 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -175,6 +175,7 @@ Singleton { property bool alwaysShowNumbers: false property int showNumberDelay: 300 // milliseconds property list numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator + property bool useNerdFont: false } property JsonObject weather: JsonObject { property bool enable: false From 3e368141c7d8d73493a8ed3c5fb10209644d46f0 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:54:11 +0200 Subject: [PATCH 60/68] bluetooth menu: round battery level --- .../sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml index 405f41c67..c2d281e2e 100644 --- a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -59,7 +59,7 @@ DialogListItem { if (!root.device?.paired) return ""; let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired"); if (!root.device?.batteryAvailable) return statusText; - statusText += ` • ${root.device?.battery * 100}%`; + statusText += ` • ${Math.round(root.device?.battery * 100)}%`; return statusText; } } From 513d140ea2f3ce2660b59ba8078e6f28a90125ba Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:39:13 +0200 Subject: [PATCH 61/68] wallpaper selector: paste directory/file to navigate --- .../modules/common/functions/StringUtils.qml | 4 ++++ .../ii/modules/overview/SearchWidget.qml | 2 +- .../WallpaperSelectorContent.qml | 21 ++++++++++++++++++- .config/quickshell/ii/services/Wallpapers.qml | 18 ++++++++++++---- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.config/quickshell/ii/modules/common/functions/StringUtils.qml b/.config/quickshell/ii/modules/common/functions/StringUtils.qml index 92de50c5c..ea5e42df9 100644 --- a/.config/quickshell/ii/modules/common/functions/StringUtils.qml +++ b/.config/quickshell/ii/modules/common/functions/StringUtils.qml @@ -217,4 +217,8 @@ Singleton { return str; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } + + function cleanCliphistEntry(str: string): string { + return str.replace(/^\d+\t/, ""); + } } diff --git a/.config/quickshell/ii/modules/overview/SearchWidget.qml b/.config/quickshell/ii/modules/overview/SearchWidget.qml index 09ae87718..fe45f9702 100644 --- a/.config/quickshell/ii/modules/overview/SearchWidget.qml +++ b/.config/quickshell/ii/modules/overview/SearchWidget.qml @@ -296,7 +296,7 @@ Item { // Wrapper return Cliphist.fuzzyQuery(searchString).map(entry => { return { cliphistRawString: entry, - name: entry.replace(/^\s*\S+\s+/, ""), + name: StringUtils.cleanCliphistEntry(entry), clickActionName: "", type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, execute: () => { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 45a08e5c8..82bf7d438 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -29,10 +29,23 @@ Item { } } + function handleFilePasting(event) { + const currentClipboardEntry = Cliphist.entries[0] + if (/^\d+\tfile:\/\/\S+/.test(currentClipboardEntry)) { + const url = StringUtils.cleanCliphistEntry(currentClipboardEntry); + Wallpapers.setDirectory(FileUtils.trimFileProtocol(decodeURIComponent(url))); + event.accepted = true; + } else { + event.accepted = false; // No image, let text pasting proceed + } + } + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { GlobalStates.wallpaperSelectorOpen = false; event.accepted = true; + } else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) { // Intercept Ctrl+V to handle "paste to go to" in pickers + root.handleFilePasting(event); } else if (event.modifiers & Qt.AltModifier && event.key === Qt.Key_Up) { Wallpapers.setDirectory(FileUtils.parentDirectory(Wallpapers.directory)); event.accepted = true; @@ -316,15 +329,21 @@ Item { } Keys.onPressed: event => { - if (text.length !== 0) { + if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) { // Intercept Ctrl+V to handle "paste to go to" in pickers + root.handleFilePasting(event); + return; + } + else if (text.length !== 0) { // No filtering, just navigate grid if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; + return; } if (event.key === Qt.Key_Up) { grid.moveSelection(-grid.columns); event.accepted = true; + return; } } event.accepted = false; diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index ebd7c27e5..f5c243234 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -76,11 +76,21 @@ Singleton { function setDirectoryIfValid(path) { validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "") if (/^\/*$/.test(validateDirProc.nicePath)) validateDirProc.nicePath = "/"; - validateDirProc.exec(["test", "-d", nicePath]) + validateDirProc.exec([ + "bash", "-c", + `if [ -d "${validateDirProc.nicePath}" ]; then echo dir; elif [ -f "${validateDirProc.nicePath}" ]; then echo file; else echo invalid; fi` + ]) } - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - root.directory = validateDirProc.nicePath + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim() + if (result === "dir") { + root.directory = validateDirProc.nicePath + } else if (result === "file") { + root.directory = FileUtils.parentDirectory(validateDirProc.nicePath) + } else { + // Ignore + } } } } From eb0bad1af643053c8b8c4597aab3d5e4a16496ad Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 17:47:37 +0200 Subject: [PATCH 62/68] wallpapers: add fallback magick thumbnailer script --- .../thumbnails/generate-thumbnails-magick.sh | 122 ++++++++++++++++++ .config/quickshell/ii/services/Wallpapers.qml | 6 +- 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100755 .config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh diff --git a/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh b/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh new file mode 100755 index 000000000..31a346ca1 --- /dev/null +++ b/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +# Generate thumbnails for files using ImageMagick, following Freedesktop spec +# Usage: +# ./generate-thumbnails-magick.sh --file +# ./generate-thumbnails-magick.sh --directory + +set -e + +# Thumbnail sizes mapping +get_thumbnail_size() { + case "$1" in + normal) echo 128 ;; + large) echo 256 ;; + x-large) echo 512 ;; + xx-large) echo 1024 ;; + *) echo 128 ;; + esac +} + +usage() { + echo "Usage: $0 --file | --directory " + exit 1 +} + +md5() { + # Calculate md5 hash of the file's absolute path + echo -n "$1" | md5sum | awk '{print $1}' +} + +urlencode() { + # Percent-encode a string for use in a URI, but do not encode slashes + local str="$1" + local encoded="" + local c + for ((i=0; i<${#str}; i++)); do + c="${str:$i:1}" + case "$c" in + [a-zA-Z0-9.~_-]|/) encoded+="$c" ;; + *) printf -v hex '%%%02X' "'${c}'"; encoded+="$hex" ;; + esac + done + echo "$encoded" +} + +generate_thumbnail() { + local src="$1" + local abs_path + abs_path="$(realpath "$src")" + local encoded_path + encoded_path="$(urlencode "$abs_path")" + local uri + uri="file://$encoded_path" + local hash + hash="$(md5 "$uri")" + local out="$CACHE_DIR/$hash.png" + mkdir -p "$CACHE_DIR" + if [ -f "$out" ]; then + return + fi + magick "$abs_path" -resize ${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE} "$out" +} + +# Parse arguments +SIZE_NAME="normal" +MODE="" +TARGET="" +while [[ $# -gt 0 ]]; do + case "$1" in + --file|-f) + MODE="file" + TARGET="$2" + shift 2 + ;; + --directory|-d) + MODE="dir" + TARGET="$2" + shift 2 + ;; + --size|-s) + SIZE_NAME="$2" + shift 2 + ;; + *) + usage + ;; + esac + # Only one mode allowed + [[ -n "$MODE" ]] && break +done + +THUMBNAIL_SIZE="$(get_thumbnail_size "$SIZE_NAME")" +CACHE_DIR="$HOME/.cache/thumbnails/$SIZE_NAME" + +if [ -z "$MODE" ] || [ -z "$TARGET" ]; then + usage +fi + +case "$MODE" in + file) + if [ ! -f "$TARGET" ]; then + echo "File not found: $TARGET" + exit 2 + fi + generate_thumbnail "$TARGET" + ;; + dir) + if [ ! -d "$TARGET" ]; then + echo "Directory not found: $TARGET" + exit 2 + fi + for f in "$TARGET"/*; do + [ -f "$f" ] || continue + generate_thumbnail "$f" & + done + wait + ;; + *) + usage + ;; +esac + diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index f5c243234..dc8734433 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,6 +15,7 @@ Singleton { id: root property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/thumbgen.py` + property string generateThumbnailsMagicScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/generate-thumbnails-magick.sh` property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) property alias folderModel: folderModel // Expose for direct binding when needed property string searchQuery: "" @@ -124,9 +125,8 @@ Singleton { thumbgenProc.directory = root.directory thumbgenProc.running = false thumbgenProc.command = [ - thumbgenScriptPath, - "--size", size, - "-d", `${root.directory}` + "bash", "-c", + `${thumbgenScriptPath} --size ${size} -d ${root.directory} || ${generateThumbnailsMagicScriptPath} --size ${size} -d ${root.directory}`, ] thumbgenProc.running = true } From 021a49a72d2ab8c55a122925f1aede27a3318dd8 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:09:05 +0200 Subject: [PATCH 63/68] wifi & bluetooth dialogs: indeterminate progressbar while scanning --- .../widgets/StyledIndeterminateProgressBar.qml | 15 +++++++++++++++ .../bluetoothDevices/BluetoothDialog.qml | 13 +++++++++++-- .../sidebarRight/wifiNetworks/WifiDialog.qml | 16 ++++++++++++---- 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml new file mode 100644 index 000000000..5dc1ef9cc --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml @@ -0,0 +1,15 @@ +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Qt5Compat.GraphicalEffects + +ProgressBar { + indeterminate: true + Material.accent: Appearance.colors.colPrimary +} diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml index 4e5d99921..2650f6d69 100644 --- a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml @@ -19,8 +19,17 @@ WindowDialog { WindowDialogTitle { text: Translation.tr("Bluetooth devices") } - // TODO: add indeterminate progress bar when scanning - WindowDialogSeparator {} + WindowDialogSeparator { + visible: !Bluetooth.defaultAdapter.discovering + } + StyledIndeterminateProgressBar { + visible: Bluetooth.defaultAdapter.discovering + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + } StyledListView { Layout.fillHeight: true Layout.fillWidth: true diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml index abd854cfc..cb7686c44 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiDialog.qml @@ -13,9 +13,18 @@ WindowDialog { WindowDialogTitle { text: Translation.tr("Connect to Wi-Fi") } - // TODO: add indeterminate progress bar when scanning - WindowDialogSeparator {} - StyledListView { + WindowDialogSeparator { + visible: !Network.wifiScanning + } + StyledIndeterminateProgressBar { + visible: Network.wifiScanning + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + Layout.leftMargin: -Appearance.rounding.large + Layout.rightMargin: -Appearance.rounding.large + } + ListView { Layout.fillHeight: true Layout.fillWidth: true Layout.topMargin: -15 @@ -25,7 +34,6 @@ WindowDialog { clip: true spacing: 0 - animateAppearance: false model: ScriptModel { values: [...Network.wifiNetworks].sort((a, b) => { From 19c321e7ae30c563ec3bf9716acc57ba128f0d65 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:10:29 +0200 Subject: [PATCH 64/68] wallpaper selector: progress indicator for thumbnail generation --- .../ii/modules/common/ThumbnailImage.qml | 5 +++-- .../WallpaperDirectoryItem.qml | 6 +++++ .../WallpaperSelectorContent.qml | 11 ++++++++-- .../thumbnails/generate-thumbnails-magick.sh | 8 ++++++- .../ii/scripts/thumbnails/thumbgen.py | 22 ++++++++++++++----- .config/quickshell/ii/services/Wallpapers.qml | 22 ++++++++++++++++++- 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.config/quickshell/ii/modules/common/ThumbnailImage.qml b/.config/quickshell/ii/modules/common/ThumbnailImage.qml index ee928eef1..506c89c41 100644 --- a/.config/quickshell/ii/modules/common/ThumbnailImage.qml +++ b/.config/quickshell/ii/modules/common/ThumbnailImage.qml @@ -16,8 +16,9 @@ Image { 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); + const resolvedUrlWithoutFileProtocol = FileUtils.trimFileProtocol(`${Qt.resolvedUrl(sourcePath)}`); + const encodedUrlWithoutFileProtocol = resolvedUrlWithoutFileProtocol.split("/").map(part => encodeURIComponent(part)).join("/"); + const md5Hash = Qt.md5(`file://${encodedUrlWithoutFileProtocol}`); return `${Directories.genericCache}/thumbnails/${thumbnailSizeName}/${md5Hash}.png`; } source: thumbnailPath diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 0b3f877f4..de87d6a24 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -80,6 +80,12 @@ MouseArea { thumbnailImage.source = ""; thumbnailImage.source = thumbnailImage.thumbnailPath; } + function onThumbnailGeneratedFile(filePath) { + if (thumbnailImage.status !== Image.Error) return; + if (Qt.resolvedUrl(thumbnailImage.sourcePath) !== Qt.resolvedUrl(filePath)) return; + thumbnailImage.source = ""; + thumbnailImage.source = thumbnailImage.thumbnailPath; + } } layer.enabled: true diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 82bf7d438..eef284c78 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -205,7 +205,9 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - StyledProgressBar { + StyledIndeterminateProgressBar { + id: indeterminateProgressBar + visible: Wallpapers.thumbnailGenerationRunning && value == 0 anchors { bottom: parent.top left: parent.left @@ -213,7 +215,12 @@ Item { leftMargin: 4 rightMargin: 4 } - indeterminate: true + } + + StyledProgressBar { + visible: Wallpapers.thumbnailGenerationRunning && value > 0 + value: Wallpapers.thumbnailGenerationProgress + anchors.fill: indeterminateProgressBar } GridView { diff --git a/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh b/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh index 31a346ca1..a5c858132 100755 --- a/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh +++ b/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh @@ -47,6 +47,12 @@ generate_thumbnail() { local src="$1" local abs_path abs_path="$(realpath "$src")" + # Skip files with multiple frames (GIFs, videos, etc.) + case "${abs_path,,}" in + *.gif|*.mp4|*.webm|*.mkv|*.avi|*.mov) + return + ;; + esac local encoded_path encoded_path="$(urlencode "$abs_path")" local uri @@ -58,7 +64,7 @@ generate_thumbnail() { if [ -f "$out" ]; then return fi - magick "$abs_path" -resize ${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE} "$out" + magick "$abs_path" -resize "${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE}" "$out" } # Parse arguments diff --git a/.config/quickshell/ii/scripts/thumbnails/thumbgen.py b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py index 4dc3ccd56..4d6326aba 100755 --- a/.config/quickshell/ii/scripts/thumbnails/thumbgen.py +++ b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py @@ -1,4 +1,4 @@ -#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@"" +#!/usr/bin/env -S\_/bin/sh\_-c\_"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. @@ -57,13 +57,22 @@ def make_thumbnail(fpath: str) -> bool: @logger.catch() -def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool) -> None: +def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool, machine_progress: bool = False) -> 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))) + if machine_progress: + completed = 0 + total = len(all_files) + with Pool(processes=workers) as p: + for result in p.imap(make_thumbnail, all_files): + completed += 1 + print(f"PROGRESS {completed}/{total} FILE {all_files[completed-1]}") + sys.stdout.flush() + else: + 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]: @@ -96,12 +105,13 @@ def get_all_files(*, dir_path: Path, recursive: bool) -> List[Path]: "-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: +@click.option("--machine_progress", is_flag=True, default=False, help="Print machine-readable progress lines instead of a progress bar") +def main(img_dirs: str, size: str, workers: str, only_images: bool, recursive: bool, machine_progress: 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) + thumbnail_folder(dir_path=img_dir, workers=workers, only_images=only_images, recursive=recursive, machine_progress=machine_progress) print("Thumbnail Generation Completed!") diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index dc8734433..910e9eaa2 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -23,9 +23,12 @@ Singleton { "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] property list wallpapers: [] // List of absolute file paths (without file://) + readonly property bool thumbnailGenerationRunning: thumbgenProc.running + property real thumbnailGenerationProgress: 0 signal changed() signal thumbnailGenerated(directory: string) + signal thumbnailGeneratedFile(filePath: string) // Executions Process { @@ -126,13 +129,30 @@ Singleton { thumbgenProc.running = false thumbgenProc.command = [ "bash", "-c", - `${thumbgenScriptPath} --size ${size} -d ${root.directory} || ${generateThumbnailsMagicScriptPath} --size ${size} -d ${root.directory}`, + `${thumbgenScriptPath} --size ${size} --machine_progress -d ${root.directory} || ${generateThumbnailsMagicScriptPath} --size ${size} -d ${root.directory}`, ] + root.thumbnailGenerationProgress = 0 thumbgenProc.running = true } Process { id: thumbgenProc property string directory + stdout: SplitParser { + onRead: data => { + // print("thumb gen proc:", data) + let match = data.match(/PROGRESS (\d+)\/(\d+)/) + if (match) { + const completed = parseInt(match[1]) + const total = parseInt(match[2]) + root.thumbnailGenerationProgress = completed / total + } + match = data.match(/FILE (.+)/) + if (match) { + const filePath = match[1] + root.thumbnailGeneratedFile(filePath) + } + } + } onExited: (exitCode, exitStatus) => { root.thumbnailGenerated(thumbgenProc.directory) } From 32f8692f132251c5453f4d07275bb93eede68456 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:10:47 +0200 Subject: [PATCH 65/68] toolbar: adjust spacing --- .config/quickshell/ii/modules/common/widgets/Toolbar.qml | 3 ++- .config/quickshell/ii/modules/common/widgets/ToolbarButton.qml | 2 -- .../quickshell/ii/modules/common/widgets/ToolbarTextField.qml | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/Toolbar.qml b/.config/quickshell/ii/modules/common/widgets/Toolbar.qml index 25a2665c4..9263ffb63 100644 --- a/.config/quickshell/ii/modules/common/widgets/Toolbar.qml +++ b/.config/quickshell/ii/modules/common/widgets/Toolbar.qml @@ -10,7 +10,7 @@ import qs.modules.common.widgets Item { id: root - property real padding: 6 + property real padding: 8 property alias colBackground: background.color default property alias data: toolbarLayout.data implicitWidth: background.implicitWidth @@ -30,6 +30,7 @@ Item { RowLayout { id: toolbarLayout + spacing: 4 anchors { fill: parent margins: root.padding diff --git a/.config/quickshell/ii/modules/common/widgets/ToolbarButton.qml b/.config/quickshell/ii/modules/common/widgets/ToolbarButton.qml index 286c4edba..04b3f5f3b 100644 --- a/.config/quickshell/ii/modules/common/widgets/ToolbarButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/ToolbarButton.qml @@ -4,7 +4,5 @@ import qs.modules.common RippleButton { Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 buttonRadius: Appearance.rounding.full } diff --git a/.config/quickshell/ii/modules/common/widgets/ToolbarTextField.qml b/.config/quickshell/ii/modules/common/widgets/ToolbarTextField.qml index fea574e96..bab455ec4 100644 --- a/.config/quickshell/ii/modules/common/widgets/ToolbarTextField.qml +++ b/.config/quickshell/ii/modules/common/widgets/ToolbarTextField.qml @@ -10,8 +10,6 @@ TextField { property alias colBackground: background.color Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 implicitWidth: 200 padding: 10 From 53b03af3e1195cbc58a786e918e050df4d0f316d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:14:32 +0200 Subject: [PATCH 66/68] wifi menu: fix undefined warnings --- .../common/widgets/StyledIndeterminateProgressBar.qml | 6 ------ .../modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml index 5dc1ef9cc..030d3fc3d 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledIndeterminateProgressBar.qml @@ -1,13 +1,7 @@ -import qs.services import qs.modules.common -import qs.modules.common.widgets import QtQuick import QtQuick.Controls.Material import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Qt5Compat.GraphicalEffects ProgressBar { indeterminate: true diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index ac178fbe2..42d9e9dd5 100644 --- a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -10,7 +10,7 @@ DialogListItem { id: root required property WifiAccessPoint wifiNetwork - active: (wifiNetwork?.askingPassword || wifiNetwork?.active) + active: (wifiNetwork?.askingPassword || wifiNetwork?.active) ?? false onClicked: { Network.connectToWifiNetwork(wifiNetwork); } @@ -93,7 +93,7 @@ DialogListItem { ColumnLayout { // Public wifi login page id: publicWifiPortal Layout.topMargin: 8 - visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0 + visible: (root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0) ?? false RowLayout { DialogButton { From f42f526f932baacdc440405d1057d9ce3cbfd24a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 31 Aug 2025 07:54:46 +0200 Subject: [PATCH 67/68] fix more bluetooth warning --- .../modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml index 2650f6d69..b9ddcaa3a 100644 --- a/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml +++ b/.config/quickshell/ii/modules/sidebarRight/bluetoothDevices/BluetoothDialog.qml @@ -20,10 +20,10 @@ WindowDialog { text: Translation.tr("Bluetooth devices") } WindowDialogSeparator { - visible: !Bluetooth.defaultAdapter.discovering + visible: !(Bluetooth.defaultAdapter?.discovering ?? false) } StyledIndeterminateProgressBar { - visible: Bluetooth.defaultAdapter.discovering + visible: Bluetooth.defaultAdapter?.discovering ?? false Layout.fillWidth: true Layout.topMargin: -8 Layout.bottomMargin: -8 From 2d52680bed30ab2257719021ff199e0cce51aa1d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 31 Aug 2025 07:56:12 +0200 Subject: [PATCH 68/68] make time correct after waking up from suspend (fixes #1905) --- .config/quickshell/ii/services/DateTime.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/DateTime.qml b/.config/quickshell/ii/services/DateTime.qml index 8a185e0bd..69bfb457f 100644 --- a/.config/quickshell/ii/services/DateTime.qml +++ b/.config/quickshell/ii/services/DateTime.qml @@ -1,3 +1,4 @@ +import qs import qs.modules.common import QtQuick import Quickshell @@ -11,7 +12,7 @@ pragma ComponentBehavior: Bound Singleton { property var clock: SystemClock { id: clock - precision: SystemClock.Minutes + precision: GlobalStates.screenLocked ? SystemClock.Seconds : SystemClock.Minutes // Hack to ensure clock is correct after waking up from suspend } property string time: Qt.locale().toString(clock.date, Config.options?.time.format ?? "hh:mm") property string shortDate: Qt.locale().toString(clock.date, Config.options?.time.shortDateFormat ?? "dd/MM")