put wallpaper picker in loader and make it use real thumbnails

This commit is contained in:
end-4
2025-08-18 20:41:44 +07:00
parent 8feee4e61a
commit 445b10d6f0
2 changed files with 334 additions and 325 deletions
@@ -11,6 +11,7 @@ Singleton {
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
@@ -14,380 +14,388 @@ import Quickshell.Hyprland
Scope { Scope {
id: scope id: scope
PanelWindow { Loader {
id: root active: GlobalStates.wallpaperOverviewOpen
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) sourceComponent: PanelWindow {
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) id: root
visible: GlobalStates.wallpaperOverviewOpen readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
property var filteredWallpapers: Wallpapers.wallpapers property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
property var filteredWallpapers: Wallpapers.wallpapers
WlrLayershell.namespace: "quickshell:wallpaper-overview" WlrLayershell.namespace: "quickshell:wallpaper-overview"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent" color: "transparent"
anchors { anchors {
top: true top: true
bottom: true bottom: true
left: true left: true
right: true right: true
} }
ColumnLayout { ColumnLayout {
id: layout id: layout
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
spacing: 8 spacing: 8
TextField { TextField {
id: filterField id: filterField
Layout.preferredWidth: bg.implicitWidth Layout.preferredWidth: bg.implicitWidth
Layout.alignment: Qt.AlignHcenter Layout.alignment: Qt.AlignHcenter
implicitHeight: 40 implicitHeight: 40
padding: 10 padding: 10
placeholderText: "Search wallpapers..." placeholderText: "Search wallpapers..."
placeholderTextColor: Appearance.colors.colSubtext placeholderTextColor: Appearance.colors.colSubtext
color: Appearance.colors.colPrimary color: Appearance.colors.colPrimary
background: Rectangle { background: Rectangle {
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.color: Appearance.colors.colLayer0Border border.color: Appearance.colors.colLayer0Border
border.width: 1 border.width: 1
radius: Appearance.rounding.small radius: Appearance.rounding.small
} }
font.family: Appearance.font.family.main font.family: Appearance.font.family.main
font.pixelSize: Appearance.font.pixelSize.normal font.pixelSize: Appearance.font.pixelSize.normal
onTextChanged: { onTextChanged: {
let newModel = []; let newModel = [];
if (text.length > 0) { if (text.length > 0) {
for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { for (let i = 0; i < Wallpapers.wallpapers.length; ++i) {
let wallpaperPath = Wallpapers.wallpapers[i]; let wallpaperPath = Wallpapers.wallpapers[i];
if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) {
newModel.push(wallpaperPath); 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; if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
} else { grid.activateCurrent();
root.filteredWallpapers = Wallpapers.wallpapers; 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 => { Rectangle {
if (text.length === 0) { id: bg
if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { focus: true
bg.forceActiveFocus(); color: Appearance.colors.colLayer0
if (event.key === Qt.Key_Down) border.width: 1
grid.moveSelection(grid.columns); border.color: Appearance.colors.colLayer0Border
else if (event.key === Qt.Key_Left) radius: Appearance.rounding.screenRounding
grid.moveSelection(-1);
else if (event.key === Qt.Key_Right) property int calculatedRows: Math.ceil(grid.count / grid.columns)
grid.moveSelection(1);
event.accepted = true; 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); grid.moveSelection(grid.columns);
event.accepted = true; event.accepted = true;
bg.forceActiveFocus(); } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
} grid.activateCurrent();
} event.accepted = true;
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { } else if (event.key === Qt.Key_Backspace) {
grid.activateCurrent(); if (filterField.text.length > 0) {
event.accepted = true; filterField.text = filterField.text.substring(0, filterField.text.length - 1);
} 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) {
filterField.forceActiveFocus(); filterField.forceActiveFocus();
event.accepted = true;
} else { } 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: 8
spacing: 8 spacing: 8
GridView { GridView {
id: grid id: grid
visible: root.filteredWallpapers.length > 0 visible: root.filteredWallpapers.length > 0
readonly property int columns: 4 readonly property int columns: 4
property int currentIndex: 0 property int currentIndex: 0
readonly property int rows: Math.max(1, Math.ceil(count / columns)) readonly property int rows: Math.max(1, Math.ceil(count / columns))
Layout.preferredWidth: columns * cellWidth Layout.preferredWidth: columns * cellWidth
Layout.alignment: Qt.AlignHcenter Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true Layout.fillHeight: true
cellWidth: 220 cellWidth: 128
cellHeight: 140 cellHeight: 72
clip: true clip: true
interactive: true interactive: true
keyNavigationWraps: true keyNavigationWraps: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: cellHeight * 2 cacheBuffer: cellHeight * 2
ScrollBar.horizontal: ScrollBar { ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded
visible: false visible: false
} }
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded
visible: false visible: false
} }
model: root.filteredWallpapers model: root.filteredWallpapers
onModelChanged: currentIndex = 0 onModelChanged: currentIndex = 0
function moveSelection(delta) { function moveSelection(delta) {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const item = itemAtIndex(i); const item = itemAtIndex(i);
if (item) { 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)); function activateCurrent() {
positionViewAtIndex(currentIndex, GridView.Contain); const path = model[currentIndex];
} if (!path)
function activateCurrent() { return;
const path = model[currentIndex]; GlobalStates.wallpaperOverviewOpen = false;
if (!path) filterField.text = "";
return; Wallpapers.apply(path);
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)
} }
Behavior on height { delegate: Item {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) width: grid.cellWidth
} height: grid.cellHeight
property bool isHovered: false
Rectangle { Behavior on width {
anchors.fill: parent animation: Appearance.animation.elementMove.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
}
Rectangle { Behavior on height {
anchors.fill: parent animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
anchors.margins: 8 }
color: Appearance.colors.colLayer2
radius: Appearance.rounding.elementRounding
Rectangle { 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 anchors.fill: parent
source: `file://${modelData}` radius: Appearance.rounding.windowRounding
fillMode: Image.PreserveAspectCrop color: Appearance.colors.colLayer1
asynchronous: true border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0
cache: false border.color: Appearance.colors.colSecondary
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 { Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true anchors.margins: 8
onEntered: { color: Appearance.colors.colLayer2
for (let i = 0; i < grid.count; i++) { radius: Appearance.rounding.elementRounding
const item = grid.itemAtIndex(i);
if (item && item !== parent) { Rectangle {
item.isHovered = false; 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 = ""; add: Transition {
Wallpapers.apply(modelData); 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 { implicitHeight: noWallpapersFoundLabel.implicitHeight
from: "*" implicitWidth: noWallpapersFoundLabel.implicitWidth
to: "*"
ParallelAnimation { Label {
PropertyAnimation { id: noWallpapersFoundLabel
property: "x" text: "No wallpapers found"
from: grid.contentX + (grid.width / 2) - width / 2 font.family: Appearance.font.family.main
} font.pixelSize: Appearance.font.pixelSize.normal
PropertyAnimation { color: Appearance.colors.colSubtext
property: "y" Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter
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 { Connections {
target: GlobalStates target: GlobalStates
function onWallpaperOverviewOpenChanged() { function onWallpaperOverviewOpenChanged() {
if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) {
filterField.forceActiveFocus(); filterField.forceActiveFocus();
}
} }
} }
} }