Files
noctalia-shell/Modules/Panels/SetupWizard/SetupWallpaperStep.qml
ItsLemmy 925bbe7a5e NImageRounded: back to using a custom shader as it looks much better than ClippingRectangle.
It seems ClippingRectangle has issues with fractional pixes.
2025-11-30 11:46:18 -05:00

435 lines
12 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../../Helpers/FuzzySort.js" as FuzzySort
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property string selectedDirectory: ""
property string selectedWallpaper: ""
signal directoryChanged(string directory)
signal wallpaperChanged(string wallpaper)
spacing: Style.marginL
// Beautiful header with icon
ColumnLayout {
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
spacing: Style.marginM
RowLayout {
spacing: Style.marginM
Rectangle {
width: 40
height: 40
radius: Style.radiusL
color: Color.mSurfaceVariant
opacity: 0.6
NIcon {
icon: "image"
pointSize: Style.fontSizeL
color: Color.mPrimary
anchors.centerIn: parent
}
}
ColumnLayout {
spacing: Style.marginXS
NText {
text: I18n.tr("setup.wallpaper.header")
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
NText {
text: I18n.tr("setup.wallpaper.subheader")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
}
}
}
// Large preview area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 180
color: Color.mSurfaceVariant
radius: Style.radiusL
// Image with rounded corners
NImageRounded {
anchors.fill: parent
visible: selectedWallpaper !== ""
imagePath: selectedWallpaper !== "" ? "file://" + selectedWallpaper : ""
radius: Style.radiusL
borderColor: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
borderWidth: selectedWallpaper !== "" ? 2 : 1
}
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
visible: selectedWallpaper === ""
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 64
height: 64
radius: width / 2
color: Color.mPrimary
NIcon {
icon: "sparkles"
pointSize: Style.fontSizeXXL
color: Color.mOnPrimary
anchors.centerIn: parent
}
}
NText {
text: I18n.tr("setup.wallpaper.select-prompt")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
font.weight: Style.fontWeightMedium
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
}
// Wallpaper gallery strip
Item {
Layout.fillWidth: true
Layout.preferredHeight: 90
visible: filteredWallpapers.length > 0
ScrollView {
id: galleryScroll
anchors.fill: parent
clip: true
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
// Enable vertical mouse wheel to scroll the horizontal strip by moving contentX
WheelHandler {
target: galleryScroll.contentItem
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
const flick = galleryScroll.contentItem;
if (!flick)
return;
const delta = event.pixelDelta.x !== 0 || event.pixelDelta.y !== 0 ? (event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.pixelDelta.x) : (event.angleDelta.y !== 0 ? event.angleDelta.y : event.angleDelta.x);
// Move opposite of wheel to scroll content to the right for wheel down
const step = -delta;
const maxX = Math.max(0, flick.contentWidth - flick.width);
let newX = flick.contentX + step;
if (newX < 0)
newX = 0;
if (newX > maxX)
newX = maxX;
flick.contentX = newX;
event.accepted = true;
}
}
RowLayout {
spacing: Style.marginM
height: parent.height
Repeater {
model: filteredWallpapers
delegate: Item {
Layout.preferredWidth: 120
Layout.preferredHeight: 80
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
border.width: selectedWallpaper === modelData ? 2 : 1
}
// Cached thumbnail
NImageCached {
id: thumbCached
anchors.fill: parent
anchors.margins: selectedWallpaper === modelData ? 2 : 1
source: "file://" + modelData
}
// Loading state
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
visible: thumbCached.status === Image.Loading
NIcon {
icon: "image"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
anchors.centerIn: parent
}
}
// Error state
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
radius: Style.radiusM
visible: thumbCached.status === Image.Error
NIcon {
icon: "image"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
anchors.centerIn: parent
}
}
NBusyIndicator {
anchors.centerIn: parent
visible: thumbCached.status === Image.Loading || thumbCached.status === Image.Null
running: visible
size: 18
}
Rectangle {
anchors.fill: parent
color: Color.mPrimary
opacity: hoverHandler.hovered ? 0.1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
Rectangle {
visible: selectedWallpaper === modelData
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 6
width: 24
height: 24
radius: width / 2
color: Color.mPrimary
NIcon {
icon: "check"
pointSize: Style.fontSizeS
color: Color.mOnPrimary
anchors.centerIn: parent
}
}
HoverHandler {
id: hoverHandler
}
TapHandler {
onTapped: {
selectedWallpaper = modelData;
wallpaperChanged(modelData);
}
}
}
}
}
}
}
// Helpful info card
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
color: Color.mSurfaceVariant
radius: Style.radiusM
opacity: 0.4
visible: filteredWallpapers.length === 0
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "folder-open"
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NText {
text: filteredWallpapers.length === 0 && selectedDirectory !== "" ? I18n.tr("setup.wallpaper.none-in-dir") : I18n.tr("setup.wallpaper.no-dir")
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Color.mOnSurfaceVariant
}
NText {
text: selectedDirectory !== "" ? I18n.tr("setup.wallpaper.no-valid", {
"dir": selectedDirectory
}) : I18n.tr("setup.wallpaper.choose-dir")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
opacity: 0.8
}
}
}
}
// Directory selection
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NTextInputButton {
id: wallpaperPathInput
label: I18n.tr("setup.wallpaper.dir.label")
description: I18n.tr("setup.wallpaper.dir.description")
text: selectedDirectory
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("setup.wallpaper.dir.browse")
Layout.fillWidth: true
onInputEditingFinished: {
selectedDirectory = text;
directoryChanged(text);
}
onButtonClicked: directoryPicker.open()
}
}
// Internal properties and functions
property list<string> wallpapersList: []
property list<string> filteredWallpapers: []
function updateFilteredWallpapers() {
filteredWallpapers = wallpapersList;
}
function refreshWallpapers() {
if (!selectedDirectory || selectedDirectory === "") {
wallpapersList = [];
filteredWallpapers = [];
return;
}
if (typeof WallpaperService !== "undefined" && WallpaperService.getWallpapersList) {
var wallpapers = WallpaperService.getWallpapersList(Screen.name);
wallpapersList = wallpapers;
updateFilteredWallpapers();
if (wallpapersList.length > 0 && selectedWallpaper === "") {
selectedWallpaper = wallpapersList[0];
}
} else {
readDirectoryImages(selectedDirectory);
}
}
function readDirectoryImages(directoryPath) {
directoryScanner.command = ["find", directoryPath, "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"];
directoryScanner.running = true;
return [];
}
onSelectedDirectoryChanged: {
if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper) {
Settings.data.wallpaper.directory = selectedDirectory;
}
if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) {
WallpaperService.refreshWallpapersList();
}
Qt.callLater(refreshWallpapers);
}
Connections {
target: WallpaperService
enabled: typeof WallpaperService !== "undefined"
function onWallpaperListChanged(screenName, count) {
if (screenName === Screen.name) {
Qt.callLater(refreshWallpapers);
}
}
}
Timer {
id: initialRefreshTimer
interval: 1000
running: false
repeat: false
onTriggered: refreshWallpapers()
}
Component.onCompleted: {
if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper && Settings.data.wallpaper.directory) {
selectedDirectory = Settings.data.wallpaper.directory;
} else {
selectedDirectory = Quickshell.env("HOME") + "/Pictures/Wallpapers";
}
if (typeof WallpaperService !== "undefined" && WallpaperService.currentWallpaper) {
selectedWallpaper = WallpaperService.currentWallpaper;
}
initialRefreshTimer.start();
}
NFilePicker {
id: directoryPicker
selectionMode: "folders"
title: I18n.tr("setup.wallpaper.dir.select-title")
initialPath: selectedDirectory || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
selectedDirectory = paths[0];
directoryChanged(paths[0]);
}
}
}
Process {
id: directoryScanner
command: ["find", "", "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"]
running: false
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: function (exitCode) {
if (exitCode === 0) {
var lines = stdout.text.split('\n');
var images = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
images.push(line);
}
}
wallpapersList = images;
updateFilteredWallpapers();
if (wallpapersList.length > 0 && selectedWallpaper === "") {
selectedWallpaper = wallpapersList[0];
}
}
}
}
}