diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/WallpaperCard.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/WallpaperCard.qml new file mode 100644 index 0000000..2f74f7e --- /dev/null +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Modules/WallpaperCard.qml @@ -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() + } + + } + +} diff --git a/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml b/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml index be6f6a1..64648cb 100644 --- a/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml +++ b/config/quickshell/.config/quickshell/Modules/Sidebar/Sidebars.qml @@ -69,6 +69,10 @@ Variants { Layout.fillWidth: true } + WallpaperCard { + Layout.fillWidth: true + } + NotificationNoteToggleCard { Layout.fillWidth: true Layout.fillHeight: true diff --git a/config/quickshell/.config/quickshell/Services/BackgroundService.qml b/config/quickshell/.config/quickshell/Services/BackgroundService.qml index 93bb070..efc421e 100644 --- a/config/quickshell/.config/quickshell/Services/BackgroundService.qml +++ b/config/quickshell/.config/quickshell/Services/BackgroundService.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import qs.Constants import qs.Services import qs.Utils @@ -56,19 +57,24 @@ Singleton { if (!exists) return ; - loadWallpaperDebouncer.pendingPath = path; + SettingsService.backgroundPath = path; loadWallpaperDebouncer.start(); }); } + function toggleChooser() { + if (wallreelProcess.running) + wallreelProcess.signal(2); + else + wallreelProcess.running = true; + } + Component.onCompleted: { - loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath; loadWallpaperDebouncer.start(); } Connections { function onBackgroundPathChanged() { - loadWallpaperDebouncer.pendingPath = SettingsService.backgroundPath; loadWallpaperDebouncer.start(); } @@ -78,15 +84,19 @@ Singleton { Timer { id: loadWallpaperDebouncer - property string pendingPath: "" - interval: 200 running: false repeat: false onTriggered: { - SettingsService.backgroundPath = pendingPath; root.loadBackground(); } } + Process { + id: wallreelProcess + + running: false + command: ["wallreel"] + } + } diff --git a/config/quickshell/.config/quickshell/Services/SettingsService.qml b/config/quickshell/.config/quickshell/Services/SettingsService.qml index c83ed73..40e1bd0 100644 --- a/config/quickshell/.config/quickshell/Services/SettingsService.qml +++ b/config/quickshell/.config/quickshell/Services/SettingsService.qml @@ -13,7 +13,10 @@ Singleton { property alias location: adapter.location property alias backgroundPath: adapter.backgroundPath 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 { id: settingFile @@ -35,7 +38,14 @@ Singleton { property string location: "New York" property string backgroundPath: "" property bool wifiEnabled: true - property list cycleWallpapers: [] + property JsonObject cycle: JsonObject { + id: cycleSettings + + property list wallpapers: [] + property bool shuffle: false + property int interval: 900 + property bool enabled: true + } } } diff --git a/config/quickshell/.config/quickshell/Services/WallpaperCycle.qml b/config/quickshell/.config/quickshell/Services/WallpaperCycle.qml index 396c190..51a4356 100644 --- a/config/quickshell/.config/quickshell/Services/WallpaperCycle.qml +++ b/config/quickshell/.config/quickshell/Services/WallpaperCycle.qml @@ -7,25 +7,133 @@ pragma Singleton Singleton { id: root - property int cycleInterval: 900 // in seconds - property var wallpapers: SettingsService.cycleWallpapers + property int cycleInterval: SettingsService.cycleInterval + property list 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() { - if (root.wallpapers.length === 0) { - Logger.w("WallpaperCycle", "No wallpapers to cycle through, skipping."); - return ; + Connections { + target: SettingsService + + function onCycleWallpapersChanged() { + _initPaths(); + _initTimer(); } - cycleTimer.stop(); - const current = SettingsService.backgroundPath; - let index = -1; - if (current) { - for (let i = 0; i < root.wallpapers.length; i++) { - if (root.wallpapers[i] === current) { - index = i; - break; - } + + function onCycleEnabledChanged() { + _initTimer(); + } + + function onCycleShuffleChanged() { + _initPaths(); + _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) { Logger.w("WallpaperCycle", "Current wallpaper not found in cycle list, starting from the beginning."); index = 0; @@ -35,21 +143,48 @@ Singleton { const nextWallpaper = root.wallpapers[index]; Logger.i("WallpaperCycle", "Cycling to next wallpaper: " + nextWallpaper); _apply(nextWallpaper); - cycleTimer.start(); + if (enabled && resetCountdown) { + _scheduleCycle(root.cycleInterval); + } + return true; } 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 { id: cycleTimer - running: true - repeat: true - interval: root.cycleInterval * 1000 + running: false + repeat: false 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(); + } } }