feat: wallpaper switcher

This commit is contained in:
2026-02-28 20:24:20 +02:00
parent 71c32d82ec
commit e5257d9e42

View File

@@ -1,5 +1,8 @@
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Services.Pipewire
import "../.."
@@ -16,6 +19,8 @@ Item {
property int menuLeft: 64
property int menuTop: 140
property int lineHeight: 38 + 40 + 1
property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight))
property int scrollOffset: 0
property int nameFontSize: 32
property int stateFontSize: 28
@@ -30,9 +35,14 @@ Item {
property ShellStateManager manager: null
property int activeSelection: 0
property bool inBluetoothMenu: false
property bool inWallpaperMenu: false
property bool isSelected: false
property string wallpapersDir: (Quickshell.env("HOME") || "") + "/Pictures/Wallpapers"
property string wallpaperCachePath: (Quickshell.env("HOME") || "") + "/.cache/.wallpaper"
property string currentWallpaperPath: ""
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
@@ -40,6 +50,8 @@ Item {
function menuLength() {
if (root.inBluetoothMenu && bluetoothRepeater)
return bluetoothRepeater.count;
if (root.inWallpaperMenu && wallpaperFolderModel)
return wallpaperFolderModel.count;
if (!menuModel)
return 0;
if (typeof menuModel.count === "function")
@@ -69,7 +81,7 @@ Item {
}
function currentAction() {
if (root.inBluetoothMenu)
if (root.inBluetoothMenu || root.inWallpaperMenu)
return null;
return menuLength() > 0 ? menuAt(activeSelection) : null;
}
@@ -78,9 +90,26 @@ Item {
const length = menuLength();
if (length === 0) {
activeSelection = 0;
scrollOffset = 0;
return;
}
activeSelection = clamp(activeSelection, 0, length - 1);
const maxOffset = Math.max(0, length - visibleRows);
scrollOffset = clamp(scrollOffset, 0, maxOffset);
}
function ensureVisible() {
if (!root.inWallpaperMenu)
return;
const length = menuLength();
if (length === 0) {
scrollOffset = 0;
return;
}
if (activeSelection < scrollOffset)
scrollOffset = activeSelection;
else if (activeSelection >= scrollOffset + visibleRows)
scrollOffset = Math.max(0, activeSelection - visibleRows + 1);
}
function bluetoothDisplayName(device) {
@@ -105,11 +134,67 @@ Item {
return item.device || null;
}
function trimText(text) {
return (text || "").replace(/^\s+|\s+$/g, "");
}
function normalizedPath(path) {
var value = trimText(path);
if (value.indexOf("file://") === 0)
value = value.slice(7);
return value;
}
function wallpaperNameAt(index) {
if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count)
return "";
var name = wallpaperFolderModel.get(index, "fileName") || "";
return name.replace(/\.[^/.]+$/, "");
}
function wallpaperPathAt(index) {
if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count)
return "";
return wallpaperFolderModel.get(index, "filePath") || "";
}
function currentWallpaperIndex() {
var current = normalizedPath(currentWallpaperPath);
if (!current || !wallpaperFolderModel)
return -1;
for (var i = 0; i < wallpaperFolderModel.count; i++) {
if (normalizedPath(wallpaperPathAt(i)) === current)
return i;
}
return -1;
}
function refreshCurrentWallpaper() {
root.currentWallpaperPath = normalizedPath(wallpaperFile.text());
}
function applyWallpaper(path) {
var normalized = normalizedPath(path);
if (!normalized || normalized.length === 0)
return;
root.currentWallpaperPath = normalized;
wallpaperFile.setText(normalized + "\n");
wallpaperApplyProcess.running = false;
wallpaperApplyProcess.environment = {
"WALLPAPER_PATH": normalized
};
wallpaperApplyProcess.command = ["bash", "-lc", "rm -rf \"$HOME/.cache/wal\"; if [[ \"$(hostname)\" != \"gentoo\" ]]; then swww img \"$WALLPAPER_PATH\" --transition-type none; else HYPRPAPER_PID=\"$(pidof hyprpaper)\"; if [ ${#HYPRPAPER_PID} -lt 1 ]; then hyprctl dispatch exec hyprpaper; sleep 1; fi; hyprctl hyprpaper unload all; hyprctl hyprpaper preload \"$WALLPAPER_PATH\"; hyprctl hyprpaper wallpaper ,\"$WALLPAPER_PATH\"; fi"];
wallpaperApplyProcess.running = true;
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
property int bluetoothActionIndex: 1
property int wallpaperActionIndex: 2
property var actions: [
{
name: "Master Volume",
@@ -132,6 +217,7 @@ Item {
name: "Bluetooth",
ent: function () {
root.inBluetoothMenu = true;
root.inWallpaperMenu = false;
root.isSelected = false;
root.activeSelection = 0;
root.clampSelection();
@@ -139,10 +225,55 @@ Item {
getState: function () {
return "";
}
},
{
name: "Wallpaper",
ent: function () {
root.inWallpaperMenu = true;
root.inBluetoothMenu = false;
root.isSelected = false;
var currentIndex = root.currentWallpaperIndex();
root.activeSelection = currentIndex >= 0 ? currentIndex : 0;
root.scrollOffset = 0;
root.clampSelection();
root.ensureVisible();
},
getState: function () {
return "";
}
}
]
property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : actions
property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : actions)
FolderListModel {
id: wallpaperFolderModel
folder: "file://" + root.wallpapersDir
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.bmp", "*.gif"]
showDirs: false
showDotAndDotDot: false
sortField: FolderListModel.Name
sortReversed: false
onCountChanged: {
if (root.inWallpaperMenu) {
root.clampSelection();
root.ensureVisible();
}
}
}
FileView {
id: wallpaperFile
path: root.wallpaperCachePath
watchChanges: true
onLoaded: root.refreshCurrentWallpaper()
onFileChanged: reload()
onLoadFailed: root.currentWallpaperPath = ""
}
Process {
id: wallpaperApplyProcess
}
Connections {
target: Bluetooth.devices
@@ -165,8 +296,31 @@ Item {
}
}
Connections {
target: wallpaperFolderModel
ignoreUnknownSignals: true
function onModelReset() {
if (root.inWallpaperMenu) {
root.clampSelection();
root.ensureVisible();
}
}
function onRowsInserted() {
if (root.inWallpaperMenu) {
root.clampSelection();
root.ensureVisible();
}
}
function onRowsRemoved() {
if (root.inWallpaperMenu) {
root.clampSelection();
root.ensureVisible();
}
}
}
Text {
text: root.inBluetoothMenu ? "BLUETOOTH" : "CONFIG"
text: root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : "CONFIG")
font.family: "8bitoperator JVE"
font.pixelSize: 71
renderType: Text.NativeRendering
@@ -186,7 +340,8 @@ Item {
width: root.width
height: lineHeight
x: 0
y: menuTop + index * lineHeight
y: menuTop + (root.inWallpaperMenu ? (index - root.scrollOffset) * lineHeight : index * lineHeight)
visible: !root.inWallpaperMenu || (index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows)
property var device: root.inBluetoothMenu ? modelData : null
@@ -202,7 +357,8 @@ Item {
Text {
x: 239
y: 0
text: root.inBluetoothMenu ? root.bluetoothDisplayName(modelData) : modelData.name
text: root.inBluetoothMenu ? root.bluetoothDisplayName(modelData) : (root.inWallpaperMenu ? root.wallpaperNameAt(index) : modelData.name)
width: root.width - 239 - (root.inWallpaperMenu ? 96 : 300)
font.family: "8bitoperator JVE"
font.pixelSize: 71
font.letterSpacing: 1
@@ -210,13 +366,16 @@ Item {
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff"
wrapMode: Text.NoWrap
elide: Text.ElideRight
color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu)) ? "#fefe00" : "#ffffff"
}
// Option state
Text {
x: menuLeft + stateColumnX
y: 4
visible: !root.inWallpaperMenu
text: root.inBluetoothMenu ? root.bluetoothDisplayState(modelData) : (modelData.getState ? modelData.getState() : "")
font.family: "8bitoperator JVE"
font.pixelSize: 71
@@ -225,7 +384,7 @@ Item {
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff"
color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu)) ? "#fefe00" : "#ffffff"
}
}
}
@@ -237,12 +396,16 @@ Item {
function handleKey(key) {
switch (key) {
case Qt.Key_Up:
if (root.inBluetoothMenu || root.isSelected === false)
if (root.inBluetoothMenu || root.inWallpaperMenu || root.isSelected === false) {
activeSelection = wrapIndex(activeSelection - 1);
root.ensureVisible();
}
return true;
case Qt.Key_Down:
if (root.inBluetoothMenu || root.isSelected === false)
if (root.inBluetoothMenu || root.inWallpaperMenu || root.isSelected === false) {
activeSelection = wrapIndex(activeSelection + 1);
root.ensureVisible();
}
return true;
case Qt.Key_Left:
{
@@ -273,6 +436,10 @@ Item {
}
// device.connected = !device.connected;
}
} else if (root.inWallpaperMenu) {
const wallpaperPath = wallpaperPathAt(activeSelection);
if (wallpaperPath && wallpaperPath.length > 0)
root.applyWallpaper(wallpaperPath);
} else {
const a = currentAction();
if (a && a.ent) {
@@ -286,7 +453,13 @@ Item {
case Qt.Key_X:
case Qt.Key_Shift:
case Qt.Key_Escape:
if (root.inBluetoothMenu) {
if (root.inWallpaperMenu) {
root.inWallpaperMenu = false;
root.isSelected = false;
root.scrollOffset = 0;
root.activeSelection = root.wallpaperActionIndex;
root.clampSelection();
} else if (root.inBluetoothMenu) {
root.inBluetoothMenu = false;
root.isSelected = false;
root.activeSelection = root.bluetoothActionIndex;
@@ -307,6 +480,9 @@ Item {
root.activeSelection = 0;
root.isSelected = false;
root.inBluetoothMenu = false;
root.inWallpaperMenu = false;
root.scrollOffset = 0;
root.refreshCurrentWallpaper();
ShellInputManager.registerHandler("quickSettings", handleKey);
}