Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/settings/WallpaperConfig.qml
Fimeg 19fe6b1d0e shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00

278 lines
11 KiB
QML

import QtQuick
import QtQuick.Layouts
import Qt.labs.folderlistmodel
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Wallpaper — pick from ~/Pictures/Wallpapers inside the settings app.
// Settings is a window in the main shell process, so applying calls the
// shell-owned Wallpapers singleton directly. Never spawn a nested `qs ipc`
// process from this page.
ContentPage {
forceWidth: true
readonly property string homeDir: Quickshell.env("HOME")
property string currentDir: homeDir + "/Pictures/Wallpapers"
property var quickDirs: [
{ name: "Wallpapers", path: homeDir + "/Pictures/Wallpapers", icon: "wallpaper" },
{ name: "Pictures", path: homeDir + "/Pictures", icon: "image" },
{ name: "Downloads", path: homeDir + "/Downloads", icon: "download" },
{ name: "Home", path: homeDir, icon: "home" }
]
ContentSection {
icon: "wallpaper"
title: Translation.tr("Wallpaper")
StyledText {
Layout.fillWidth: true
text: Translation.tr("Current: %1").arg(
(Config.options.background.wallpaperPath || Translation.tr("none")).split("/").pop())
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideMiddle
}
// Content filter — which wallhaven categories the random pick may
// include. Persisted to config.json where the download script reads it.
StyledText {
Layout.fillWidth: true
text: Translation.tr("Allowed content")
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
}
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: [
{ key: "sfw", label: Translation.tr("SFW") },
{ key: "sketchy", label: Translation.tr("Sketchy") },
{ key: "nsfw", label: Translation.tr("NSFW") }
]
RippleButton {
id: purityChip
required property var modelData
property bool on: modelData.key === "sfw" ? WallpaperDownload.puritySfw
: modelData.key === "sketchy" ? WallpaperDownload.puritySketchy
: WallpaperDownload.purityNsfw
padding: 8
buttonRadius: Appearance.rounding.small
toggled: on
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: {
if (modelData.key === "sfw")
WallpaperDownload.puritySfw = !WallpaperDownload.puritySfw;
else if (modelData.key === "sketchy")
WallpaperDownload.puritySketchy = !WallpaperDownload.puritySketchy;
else
WallpaperDownload.purityNsfw = !WallpaperDownload.purityNsfw;
WallpaperDownload.savePurity();
}
contentItem: RowLayout {
spacing: 4
MaterialSymbol {
iconSize: 16
text: purityChip.on ? "check" : "add"
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: modelData.label
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
}
}
}
}
// Download a fresh random wallpaper from wallhaven. Downloads land in
// ~/Pictures/Wallpapers (each a distinct wallhaven_<id> file), then
// apply through the shell like any picked image.
RippleButton {
Layout.fillWidth: true
padding: 10
buttonRadius: Appearance.rounding.small
enabled: !WallpaperDownload.downloading
onClicked: WallpaperDownload.download()
contentItem: RowLayout {
spacing: 8
MaterialSymbol {
iconSize: 20
text: WallpaperDownload.downloading ? "hourglass_top" : "cloud_download"
color: Appearance.colors.colOnLayer1
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
text: WallpaperDownload.downloading
? Translation.tr("Downloading…")
: Translation.tr("Download random wallpaper")
color: Appearance.colors.colOnLayer1
}
}
}
StyledText {
visible: WallpaperDownload.lastError.length > 0
Layout.fillWidth: true
text: WallpaperDownload.lastError
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
// Quick directory buttons
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: quickDirs
RippleButton {
required property var modelData
property bool isCurrent: currentDir === modelData.path
padding: 8
buttonRadius: Appearance.rounding.small
toggled: isCurrent
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: {
currentDir = modelData.path;
}
contentItem: RowLayout {
spacing: 4
MaterialSymbol {
iconSize: 16
text: modelData.icon
color: isCurrent ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: modelData.name
color: isCurrent ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
}
}
}
}
// Current path display
StyledText {
Layout.fillWidth: true
text: currentDir.replace(homeDir, "~")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideMiddle
}
GridView {
id: grid
Layout.fillWidth: true
// Rows of ~3 across the phone width; height fits the model.
readonly property int cols: 3
cellWidth: Math.floor(width / cols)
cellHeight: Math.floor(cellWidth * 2) // portrait-ish tiles
implicitHeight: Math.ceil(folderModel.count / cols) * cellHeight
interactive: false // the page scrolls, not the grid
clip: true
model: FolderListModel {
id: folderModel
folder: "file://" + currentDir
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.avif"]
showDirs: true
showDotAndDotDot: false
showOnlyReadable: true
sortField: FolderListModel.Name
}
delegate: Item {
required property string filePath
required property string fileName
required property bool fileIsDir
width: grid.cellWidth
height: grid.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
border.width: (!fileIsDir && Config.options.background.wallpaperPath === filePath) ? 3 : 0
border.color: Appearance.colors.colPrimary
clip: true
Image {
anchors.fill: parent
anchors.margins: 3
source: fileIsDir ? "" : "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
sourceSize.width: 240
visible: !fileIsDir
}
// Directory indicator
Column {
anchors.centerIn: parent
visible: fileIsDir
spacing: 4
MaterialSymbol {
anchors.horizontalCenter: parent.horizontalCenter
iconSize: 32
text: "folder"
color: Appearance.colors.colPrimary
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Appearance.font.pixelSize.smaller
text: fileName
color: Appearance.colors.colOnLayer1
elide: Text.ElideRight
width: grid.cellWidth - 16
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (fileIsDir) {
currentDir = filePath;
} else {
// Shell process owns selection + theming.
Wallpapers.apply(filePath);
Config.options.background.wallpaperPath = filePath;
}
}
}
}
}
}
StyledText {
visible: folderModel.count === 0
Layout.fillWidth: true
text: Translation.tr("No images in %1").arg(currentDir.replace(homeDir, "~"))
color: Appearance.colors.colSubtext
wrapMode: Text.WordWrap
}
}
}