mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-24 12:22:09 +00:00
440 lines
22 KiB
QML
440 lines
22 KiB
QML
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 real previewCellAspectRatio: 4 / 3
|
|
implicitHeight: columnLayout.implicitHeight
|
|
implicitWidth: columnLayout.implicitWidth
|
|
property var wallpapers: 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
|
|
anchors.fill: parent
|
|
spacing: -Appearance.sizes.elevationMargin
|
|
|
|
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
|
|
|
|
ColumnLayout {
|
|
// The grid
|
|
anchors.fill: parent
|
|
|
|
AddressBar {
|
|
id: addressBar
|
|
Layout.margins: 4
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: false
|
|
directory: Wallpapers.directory
|
|
onNavigateToDirectory: path => {
|
|
Wallpapers.directory = path;
|
|
}
|
|
radius: wallpaperGridBackground.radius - Layout.margins
|
|
}
|
|
|
|
Item {
|
|
id: gridDisplayRegion
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
layer.enabled: true
|
|
layer.effect: OpacityMask {
|
|
maskSource: Rectangle {
|
|
width: gridDisplayRegion.width
|
|
height: gridDisplayRegion.height
|
|
radius: wallpaperGridBackground.radius
|
|
}
|
|
}
|
|
|
|
GridView {
|
|
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() {
|
|
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: 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
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
|
|
}
|
|
|
|
Rectangle { // Bottom toolbar
|
|
id: extraOptionsBackground
|
|
property real padding: 6
|
|
anchors {
|
|
fill: parent
|
|
margins: 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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: GlobalStates
|
|
function onWallpaperSelectorOpenChanged() {
|
|
if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) {
|
|
filterField.forceActiveFocus();
|
|
}
|
|
}
|
|
}
|
|
}
|