feat: implement wallpaper cycling with user controls in sidebar

This commit is contained in:
2026-03-11 23:32:30 +01:00
parent b84f571d4e
commit aab409ecc3
5 changed files with 275 additions and 29 deletions
@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Components
import qs.Constants
import qs.Services
RowLayout {
implicitHeight: wallpaperImage.implicitHeight
spacing: Style.marginS
UImageRounded {
id: wallpaperImage
Layout.fillWidth: true
height: Style.baseWidgetSize * 3.2 + Style.marginS * 3
radius: Style.radiusM
imagePath: BackgroundService.cachedPath
fallbackIcon: "wallpaper"
layer.enabled: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: BackgroundService.toggleChooser()
cursorShape: Qt.PointingHandCursor
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: Style.marginS
height: parent.height * WallpaperCycle.timeUntilNextCycle / WallpaperCycle.cycleInterval
color: Colors.mPrimary
radius: Style.radiusS
}
layer.effect: MultiEffect {
shadowEnabled: true
blurMax: Style.shadowBlurMax
shadowBlur: Style.shadowBlur
shadowOpacity: Style.shadowOpacity
shadowColor: Colors.mShadow
shadowHorizontalOffset: Style.shadowHorizontalOffset
shadowVerticalOffset: Style.shadowVerticalOffset
}
}
ColumnLayout {
id: buttonsColumn
spacing: Style.marginS
UIconButton {
baseSize: Style.baseWidgetSize * 0.8
iconName: "player-track-prev"
colorFg: Colors.mBlue
onClicked: WallpaperCycle.applyPrev()
}
UIconButton {
baseSize: Style.baseWidgetSize * 0.8
iconName: "player-track-next"
colorFg: Colors.mYellow
onClicked: WallpaperCycle.applyNext()
}
UIconButton {
baseSize: Style.baseWidgetSize * 0.8
iconName: WallpaperCycle.enabled ? "player-pause" : "player-play"
colorFg: WallpaperCycle.enabled ? Colors.mGreen : Colors.mRed
alwaysHover: !WallpaperCycle.enabled
onClicked: WallpaperCycle.playPause()
}
UIconButton {
baseSize: Style.baseWidgetSize * 0.8
iconName: WallpaperCycle.shuffle ? "arrows-shuffle" : "repeat"
colorFg: Colors.mPurple
alwaysHover: WallpaperCycle.shuffle
onClicked: WallpaperCycle.toggleShuffle()
}
}
}
@@ -69,6 +69,10 @@ Variants {
Layout.fillWidth: true Layout.fillWidth: true
} }
WallpaperCard {
Layout.fillWidth: true
}
NotificationNoteToggleCard { NotificationNoteToggleCard {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Constants import qs.Constants
import qs.Services import qs.Services
import qs.Utils import qs.Utils
@@ -56,19 +57,24 @@ Singleton {
if (!exists) if (!exists)
return ; return ;
loadWallpaperDebouncer.pendingPath = path; SettingsService.backgroundPath = path;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
}); });
} }
function toggleChooser() {
if (wallreelProcess.running)
wallreelProcess.signal(2);
else
wallreelProcess.running = true;
}
Component.onCompleted: { Component.onCompleted: {
loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
} }
Connections { Connections {
function onBackgroundPathChanged() { function onBackgroundPathChanged() {
loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath;
loadWallpaperDebouncer.start(); loadWallpaperDebouncer.start();
} }
@@ -78,15 +84,19 @@ Singleton {
Timer { Timer {
id: loadWallpaperDebouncer id: loadWallpaperDebouncer
property string pendingPath: ""
interval: 200 interval: 200
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
SettingsService.backgroundPath = pendingPath;
root.loadBackground(); root.loadBackground();
} }
} }
Process {
id: wallreelProcess
running: false
command: ["wallreel"]
}
} }
@@ -13,7 +13,10 @@ Singleton {
property alias location: adapter.location property alias location: adapter.location
property alias backgroundPath: adapter.backgroundPath property alias backgroundPath: adapter.backgroundPath
property alias wifiEnabled: adapter.wifiEnabled property alias wifiEnabled: adapter.wifiEnabled
property alias cycleWallpapers: adapter.cycleWallpapers property alias cycleWallpapers: cycleSettings.wallpapers
property alias cycleShuffle: cycleSettings.shuffle
property alias cycleInterval: cycleSettings.interval
property alias cycleEnabled: cycleSettings.enabled
FileView { FileView {
id: settingFile id: settingFile
@@ -35,7 +38,14 @@ Singleton {
property string location: "New York" property string location: "New York"
property string backgroundPath: "" property string backgroundPath: ""
property bool wifiEnabled: true property bool wifiEnabled: true
property list<string> cycleWallpapers: [] property JsonObject cycle: JsonObject {
id: cycleSettings
property list<string> wallpapers: []
property bool shuffle: false
property int interval: 900
property bool enabled: true
}
} }
} }
@@ -7,25 +7,133 @@ pragma Singleton
Singleton { Singleton {
id: root id: root
property int cycleInterval: 900 // in seconds property int cycleInterval: SettingsService.cycleInterval
property var wallpapers: SettingsService.cycleWallpapers property list<string> wallpapers: []
property bool enabled: SettingsService.cycleEnabled
property bool shuffle: SettingsService.cycleShuffle
property int timeUntilNextCycle: SettingsService.cycleInterval
property double nextCycleDeadlineMs: 0
property int _startIndex: 0
function applyNext() { Connections {
if (root.wallpapers.length === 0) { target: SettingsService
Logger.w("WallpaperCycle", "No wallpapers to cycle through, skipping.");
return ; function onCycleWallpapersChanged() {
_initPaths();
_initTimer();
} }
cycleTimer.stop();
const current = SettingsService.backgroundPath; function onCycleEnabledChanged() {
let index = -1; _initTimer();
if (current) { }
for (let i = 0; i < root.wallpapers.length; i++) {
if (root.wallpapers[i] === current) { function onCycleShuffleChanged() {
index = i; _initPaths();
break; _initTimer();
} }
function onCycleIntervalChanged() {
if (enabled && cycleTimer.running) {
_scheduleCycle(root.cycleInterval);
} }
} }
}
Component.onCompleted: {
_initPaths();
_initTimer();
}
function _initPaths() {
if (!shuffle) {
wallpapers = SettingsService.cycleWallpapers;
} else {
wallpapers = _shuffle(SettingsService.cycleWallpapers);
}
Logger.d("WallpaperCycle", "Initialized wallpapers: " + wallpapers.length + " paths" + (shuffle ? " (shuffled)" : ""));
}
function _initTimer() {
if (enabled && root.wallpapers.length > 0) {
const remainingSeconds = Math.max(1, nextCycleDeadlineMs > 0
? _secondsUntilDeadline()
: root.timeUntilNextCycle);
_scheduleCycle(remainingSeconds);
} else {
if (cycleTimer.running && nextCycleDeadlineMs > 0) {
timeUntilNextCycle = _secondsUntilDeadline();
}
cycleTimer.stop();
nextCycleDeadlineMs = 0;
}
}
function _scheduleCycle(secondsFromNow) {
const seconds = Math.max(1, Math.floor(secondsFromNow));
timeUntilNextCycle = seconds;
nextCycleDeadlineMs = Date.now() + (seconds * 1000);
cycleTimer.interval = seconds * 1000;
cycleTimer.stop();
cycleTimer.start();
Logger.d("WallpaperCycle", "Scheduled next cycle in " + seconds + " seconds.");
}
function _secondsUntilDeadline() {
return Math.max(0, Math.ceil((nextCycleDeadlineMs - Date.now()) / 1000));
}
function _shuffle(paths) {
const shuffled = paths.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
function playPause() {
SettingsService.cycleEnabled = !SettingsService.cycleEnabled;
}
function toggleShuffle() {
SettingsService.cycleShuffle = !SettingsService.cycleShuffle;
}
function applyPrev(resetCountdown) {
if (resetCountdown === undefined)
resetCountdown = true;
if (root.wallpapers.length === 0) {
Logger.w("WallpaperCycle", "No wallpapers to cycle through, skipping.");
return false;
}
cycleTimer.stop();
let index = _findIndex(SettingsService.backgroundPath);
if (index === -1) {
Logger.w("WallpaperCycle", "Current wallpaper not found in cycle list, starting from the end.");
index = root.wallpapers.length - 1;
} else {
index = (index - 1 + root.wallpapers.length) % root.wallpapers.length;
}
const prevWallpaper = root.wallpapers[index];
Logger.i("WallpaperCycle", "Cycling to previous wallpaper: " + prevWallpaper);
_apply(prevWallpaper);
if (enabled && resetCountdown) {
_scheduleCycle(root.cycleInterval);
}
return true;
}
function applyNext(resetCountdown) {
if (resetCountdown === undefined)
resetCountdown = true;
if (root.wallpapers.length === 0) {
Logger.w("WallpaperCycle", "No wallpapers to cycle through, skipping.");
return false;
}
cycleTimer.stop();
let index = _findIndex(SettingsService.backgroundPath);
if (index === -1) { if (index === -1) {
Logger.w("WallpaperCycle", "Current wallpaper not found in cycle list, starting from the beginning."); Logger.w("WallpaperCycle", "Current wallpaper not found in cycle list, starting from the beginning.");
index = 0; index = 0;
@@ -35,21 +143,48 @@ Singleton {
const nextWallpaper = root.wallpapers[index]; const nextWallpaper = root.wallpapers[index];
Logger.i("WallpaperCycle", "Cycling to next wallpaper: " + nextWallpaper); Logger.i("WallpaperCycle", "Cycling to next wallpaper: " + nextWallpaper);
_apply(nextWallpaper); _apply(nextWallpaper);
cycleTimer.start(); if (enabled && resetCountdown) {
_scheduleCycle(root.cycleInterval);
}
return true;
} }
function _apply(path) { function _apply(path) {
Quickshell.execDetached(["sh", "-c", "wallreel -a '" + path + "'"]); Logger.d("WallpaperCycle", "Applying wallpaper: " + path);
Quickshell.execDetached(["wallreel", "-a", path]);
}
function _findIndex(path) {
for (let i = 0; i < root.wallpapers.length; i++) {
if (root.wallpapers[i] === path)
return i;
}
return -1;
} }
Timer { Timer {
id: cycleTimer id: cycleTimer
running: true running: false
repeat: true repeat: false
interval: root.cycleInterval * 1000
onTriggered: { onTriggered: {
root.applyNext(); const applied = root.applyNext(false);
if (applied && root.enabled) {
_scheduleCycle(root.cycleInterval);
}
}
}
Timer {
id: updateTimeTimer
running: cycleTimer.running
repeat: true
interval: 1000
onTriggered: {
if (nextCycleDeadlineMs > 0) {
timeUntilNextCycle = _secondsUntilDeadline();
}
} }
} }