Adds a Download-random-wallpaper button and a 3-way content filter (SFW/Sketchy/NSFW) to our Wallpaper settings page, backed by a new WallpaperDownload service and its own download script under surfaces/quickshell/scripts/wallpaper/ — not ii's scripts/colors tree, which is going away. Only the 'wallpapers apply' IPC target still comes from ii and moves with it when the base is vendored. Downloads are named by wallhaven id so the library accumulates instead of overwriting a single wallhaven_wallpaper.<ext>. Purity is read from and written to background.wallhaven.purity in config.json (not in the upstream Config.options schema). Anchors the root scripts/ gitignore to / so shipped surface scripts are tracked.
279 lines
11 KiB
QML
279 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.
|
|
// Applying goes through the main shell's `wallpapers apply` IPC so the
|
|
// selection, config persistence, and matugen theming all run in the shell
|
|
// process that owns them; this page is only a view.
|
|
|
|
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.
|
|
Quickshell.execDetached(["qs", "-c", "souveraine",
|
|
"ipc", "call", "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
|
|
}
|
|
}
|
|
}
|