From 6d591dcaa77ce9d51934b1073ce95c78f13797f9 Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Sun, 2 Nov 2025 13:22:35 -0500 Subject: [PATCH 1/6] Add support for monitors with higher peak brightness (not parsed) --- services/Brightness.qml | 59 ++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index ac905fd8e..4f795a4a3 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -78,10 +78,18 @@ Singleton { command: ["ddcutil", "detect", "--brief"] stdout: StdioCollector { - onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ - busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], - connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" - })) + onStreamFinished: { + const blocks = text.trim().split("\n\n").filter(d => d.startsWith("Display ")); + root.ddcMonitors = blocks.map(d => { + const busMatch = d.match(/I2C bus:\s*\/dev\/i2c-([0-9]+)/i); + // Accept both "DRM connector:" and "DRM_connector:" + const connMatch = d.match(/DRM[_ ]connector:\s+(.*)/i); + return { + busNum: busMatch ? busMatch[1] : "", + connector: connMatch ? connMatch[1].replace(/^card\d+-/, "") : "" + }; + }).filter(m => m.busNum && m.connector); + } } } @@ -159,16 +167,33 @@ Singleton { readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") property real brightness property real queuedBrightness: NaN + // Default to 250 for your Dell AW3423DW; override with parsed max if available. + property int vcpMax: 250 readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { + // Extract integers defensively; works for ddcutil and brightnessctl echoes + const nums = (text.match(/\d+/g) ?? []).map(n => parseInt(n, 10)).filter(n => !isNaN(n)); if (monitor.isAppleDisplay) { - const val = parseInt(text.trim()); - monitor.brightness = val / 101; + const val = nums.at(-1) ?? 0; + monitor.vcpMax = 100; // Apple path writes 0..100 + monitor.brightness = val / 101; // keep original behavior + } else if (monitor.isDdc) { + // ddcutil getvcp 10 --brief ends with "... cur max" + const cur = nums.at(-2); + const max = nums.at(-1); + // If parsing succeeds, use reported max; otherwise keep 250 fallback. + if (!isNaN(max)) monitor.vcpMax = max; + monitor.brightness = (!isNaN(cur) && !isNaN(monitor.vcpMax) && monitor.vcpMax > 0) + ? cur / monitor.vcpMax + : 0; } else { - const [, , , cur, max] = text.split(" "); - monitor.brightness = parseInt(cur) / parseInt(max); + // brightnessctl path: our echo prints ... at the end + const cur = nums.at(-2) ?? 0; + const max = nums.at(-1) ?? 100; + monitor.vcpMax = 100; // writes use % for brightnessctl + monitor.brightness = max > 0 ? cur / max : 0; } } } @@ -186,8 +211,8 @@ Singleton { function setBrightness(value: real): void { value = Math.max(0, Math.min(1, value)); - const rounded = Math.round(value * 100); - if (Math.round(brightness * 100) === rounded) + const scaled100 = Math.round(value * 100); + if (Math.round((brightness ?? 0) * 100) === scaled100) return; if (isDdc && timer.running) { @@ -197,12 +222,14 @@ Singleton { brightness = value; - if (isAppleDisplay) - Quickshell.execDetached(["asdbctl", "set", rounded]); - else if (isDdc) - Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]); - else - Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); + if (isAppleDisplay) { + Quickshell.execDetached(["asdbctl", "set", Math.round(value * 100)]); + } else if (isDdc) { + // Write using vcpMax (defaults to 250; uses parsed max when available) + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", Math.round(value * vcpMax)]); + } else { + Quickshell.execDetached(["brightnessctl", "s", `${scaled100}%`]); + } if (isDdc) timer.restart(); From 5a148866631659eb42d0743e2b87285b192b062c Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Sun, 2 Nov 2025 16:36:34 -0500 Subject: [PATCH 2/6] Use old initializer --- services/Brightness.qml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index 4f795a4a3..0038b6060 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -180,14 +180,8 @@ Singleton { monitor.vcpMax = 100; // Apple path writes 0..100 monitor.brightness = val / 101; // keep original behavior } else if (monitor.isDdc) { - // ddcutil getvcp 10 --brief ends with "... cur max" - const cur = nums.at(-2); - const max = nums.at(-1); - // If parsing succeeds, use reported max; otherwise keep 250 fallback. - if (!isNaN(max)) monitor.vcpMax = max; - monitor.brightness = (!isNaN(cur) && !isNaN(monitor.vcpMax) && monitor.vcpMax > 0) - ? cur / monitor.vcpMax - : 0; + const [, , , cur, max] = text.split(" "); + monitor.brightness = parseInt(cur) / parseInt(max); } else { // brightnessctl path: our echo prints ... at the end const cur = nums.at(-2) ?? 0; From cbc9bbb28a09f67905ad9dc84ad6bf5f609fe95a Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Thu, 6 Nov 2025 21:09:47 -0500 Subject: [PATCH 3/6] working first draft (need to tweak) --- components/images/CachingImage.qml | 85 ++++++++++++++++++- modules/background/Wallpaper.qml | 16 +++- modules/launcher/items/WallpaperItem.qml | 1 + .../Internal/cachingimagemanager.cpp | 53 +++++++++++- .../Internal/cachingimagemanager.hpp | 12 ++- utils/Images.qml | 4 +- 6 files changed, 159 insertions(+), 12 deletions(-) diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index e8f957a7d..fb79608d3 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -3,19 +3,96 @@ import Caelestia.Internal import Quickshell import QtQuick -Image { +Item { id: root property alias path: manager.path - asynchronous: true - fillMode: Image.PreserveAspectCrop + property url source: "" + property size sourceSize: Qt.size(0, 0) + + property int fillMode: Image.PreserveAspectCrop + property bool smooth: true + property bool asynchronous: true + + property bool playbackEnabled: true + property bool pauseWhenHidden: true + + readonly property bool animated: manager.animated + readonly property Item contentItem: loader.status === Loader.Ready ? loader.item : null + readonly property int status: contentItem ? contentItem.status : Image.Null + + implicitWidth: 0 + implicitHeight: 0 + + function restart(): void { + if (!animated || !contentItem || !("currentFrame" in contentItem)) + return; + + contentItem.currentFrame = 0; + } + + function updateAnimatedPause(): void { + if (!animated || !contentItem || !("paused" in contentItem)) + return; + + const shouldPause = !playbackEnabled || (pauseWhenHidden && !visible); + if (contentItem.paused !== shouldPause) + contentItem.paused = shouldPause; + } + + onPlaybackEnabledChanged: updateAnimatedPause() + onPauseWhenHiddenChanged: updateAnimatedPause() + onVisibleChanged: updateAnimatedPause() + onAnimatedChanged: updateAnimatedPause() Connections { target: QsWindow.window function onDevicePixelRatioChanged(): void { - manager.updateSource(); + if (!manager.animated) + manager.updateSource(); + } + } + + Loader { + id: loader + + anchors.fill: parent + active: !!root.path + asynchronous: true + + sourceComponent: manager.animated ? animatedComponent : staticComponent + + onStatusChanged: { + if (status === Loader.Ready) + root.updateAnimatedPause(); + } + } + + Component { + id: staticComponent + + Image { + anchors.fill: parent + asynchronous: root.asynchronous + fillMode: root.fillMode + smooth: root.smooth + source: root.source + sourceSize: root.sourceSize + } + } + + Component { + id: animatedComponent + + AnimatedImage { + anchors.fill: parent + cache: false + fillMode: root.fillMode + smooth: root.smooth + source: root.source + sourceSize: root.sourceSize } } diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 233dacb72..489deb9ef 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -12,7 +12,8 @@ Item { id: root property string source: Wallpapers.current - property Image current: one + property CachingImage current: one + readonly property Item imageItem: (current && current.contentItem) ? current.contentItem : null anchors.fill: parent @@ -122,10 +123,19 @@ Item { opacity: 0 scale: Wallpapers.showPreview ? 1 : 0.8 + playbackEnabled: root.current === img onStatusChanged: { - if (status === Image.Ready) - root.current = this; + if (status === Image.Ready) root.current = this; + } + + Connections { + target: root + + function onCurrentChanged(): void { + if (root.current === img && img.animated) + img.restart(); + } } states: State { diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 1128bad21..9403e369c 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -67,6 +67,7 @@ Item { CachingImage { path: root.modelData.path smooth: !root.PathView.view.moving + playbackEnabled: false anchors.fill: parent } diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 1c15cd203..a95ebad9a 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -88,6 +88,13 @@ void CachingImageManager::setPath(const QString& path) { m_path = path; emit pathChanged(); + // eww but I'll do it again here + const bool animated = !path.isEmpty() && isAnimated(path); + if (m_animated != animated) { + m_animated = animated; + emit animatedChanged(); + } + if (!path.isEmpty()) { updateSource(path); } @@ -98,8 +105,36 @@ void CachingImageManager::updateSource() { } void CachingImageManager::updateSource(const QString& path) { - if (path.isEmpty() || path == m_shaPath) { - // Path is empty or already calculating sha for path + if (path.isEmpty()) { + return; + } + + const bool animated = isAnimated(path); + if (m_animated != animated) { + m_animated = animated; + emit animatedChanged(); + } + + if (animated) { + const QSize size = effectiveSize(); + + if (!m_item || !size.width() || !size.height()) { + m_shaPath.clear(); + return; + } + + const QUrl cache; + if (m_cachePath != cache) { + m_cachePath = cache; + emit cachePathChanged(); + } + + m_item->setProperty("source", QUrl::fromLocalFile(path)); + m_shaPath.clear(); + return; + } + + if (path == m_shaPath) { return; } @@ -170,6 +205,12 @@ QUrl CachingImageManager::cachePath() const { void CachingImageManager::createCache( const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { + // Main thread is fast as fuk boiiii + if (isAnimated(path)) { + qDebug() << "CachingImageManager::createCache: skip animated" << path; + return; + } + QThreadPool::globalInstance()->start([path, cache, fillMode, size] { QImage image(path); @@ -220,4 +261,12 @@ QString CachingImageManager::sha256sum(const QString& path) { return hash.result().toHex(); } +bool CachingImageManager::isAnimated(const QString& path) { + QImageReader reader(path); + if (!reader.canRead() || !reader.supportsAnimation()) { + return false; + } + return reader.imageCount() > 1; +} + } // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 3611699b6..1271a6212 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -16,10 +16,13 @@ class CachingImageManager : public QObject { Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) + Q_PROPERTY(bool animated READ animated NOTIFY animatedChanged) + public: explicit CachingImageManager(QObject* parent = nullptr) : QObject(parent) - , m_item(nullptr) {} + , m_item(nullptr) + , m_animated(false) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); @@ -32,6 +35,8 @@ class CachingImageManager : public QObject { [[nodiscard]] QUrl cachePath() const; + [[nodiscard]] bool animated() const { return m_animated; } + Q_INVOKABLE void updateSource(); Q_INVOKABLE void updateSource(const QString& path); @@ -42,6 +47,7 @@ class CachingImageManager : public QObject { void pathChanged(); void cachePathChanged(); void usingCacheChanged(); + void animatedChanged(); private: QString m_shaPath; @@ -52,6 +58,8 @@ class CachingImageManager : public QObject { QString m_path; QUrl m_cachePath; + bool m_animated; + QMetaObject::Connection m_widthConn; QMetaObject::Connection m_heightConn; @@ -59,6 +67,8 @@ class CachingImageManager : public QObject { [[nodiscard]] QSize effectiveSize() const; void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; + + [[nodiscard]] static bool isAnimated(const QString& path); [[nodiscard]] static QString sha256sum(const QString& path); }; diff --git a/utils/Images.qml b/utils/Images.qml index ac76f5118..6cfbcd06f 100644 --- a/utils/Images.qml +++ b/utils/Images.qml @@ -3,8 +3,8 @@ pragma Singleton import Quickshell Singleton { - readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] - readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg", "gif"] + readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg", "gif"] function isValidImageByName(name: string): bool { return validImageExtensions.some(t => name.endsWith(`.${t}`)); From 711a905f325ed9bf8fe8819f2453afa1cb561a8f Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Fri, 7 Nov 2025 00:01:42 -0500 Subject: [PATCH 4/6] animated / static wallpaper preview / thumbnail toggle --- components/images/CachingImage.qml | 6 +++-- modules/launcher/items/WallpaperItem.qml | 6 ++++- .../Internal/cachingimagemanager.cpp | 23 +++++++++++++------ .../Internal/cachingimagemanager.hpp | 5 ++++ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index fb79608d3..0d4b69f54 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -17,6 +17,7 @@ Item { property bool playbackEnabled: true property bool pauseWhenHidden: true + property bool preferAnimated: true readonly property bool animated: manager.animated readonly property Item contentItem: loader.status === Loader.Ready ? loader.item : null @@ -50,7 +51,7 @@ Item { target: QsWindow.window function onDevicePixelRatioChanged(): void { - if (!manager.animated) + if (!manager.animated || !root.preferAnimated) manager.updateSource(); } } @@ -62,7 +63,7 @@ Item { active: !!root.path asynchronous: true - sourceComponent: manager.animated ? animatedComponent : staticComponent + sourceComponent: manager.animated && root.preferAnimated ? animatedComponent : staticComponent onStatusChanged: { if (status === Loader.Ready) @@ -101,5 +102,6 @@ Item { item: root cacheDir: Qt.resolvedUrl(Paths.imagecache) + preferAnimated: root.preferAnimated } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 9403e369c..e621975cb 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -13,6 +13,9 @@ Item { required property FileSystemEntry modelData required property PersistentProperties visibilities + // Play the animated wallpaper preview? + property bool animatePreview: false + scale: 0.5 opacity: 0 z: PathView.z ?? 0 @@ -67,7 +70,8 @@ Item { CachingImage { path: root.modelData.path smooth: !root.PathView.view.moving - playbackEnabled: false + preferAnimated: root.animatePreview + playbackEnabled: root.animatePreview anchors.fill: parent } diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index a95ebad9a..6c6ddaf72 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -100,6 +100,19 @@ void CachingImageManager::setPath(const QString& path) { } } +void CachingImageManager::setPreferAnimated(bool preferAnimated) { + if (m_preferAnimated == preferAnimated) { + return; + } + + m_preferAnimated = preferAnimated; + emit preferAnimatedChanged(); + + if (!m_path.isEmpty()) { + updateSource(m_path); + } +} + void CachingImageManager::updateSource() { updateSource(m_path); } @@ -115,7 +128,9 @@ void CachingImageManager::updateSource(const QString& path) { emit animatedChanged(); } - if (animated) { + const bool useAnimation = animated && m_preferAnimated; + + if (useAnimation) { const QSize size = effectiveSize(); if (!m_item || !size.width() || !size.height()) { @@ -205,12 +220,6 @@ QUrl CachingImageManager::cachePath() const { void CachingImageManager::createCache( const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { - // Main thread is fast as fuk boiiii - if (isAnimated(path)) { - qDebug() << "CachingImageManager::createCache: skip animated" << path; - return; - } - QThreadPool::globalInstance()->start([path, cache, fillMode, size] { QImage image(path); diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 1271a6212..5f0882ce7 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -17,6 +17,7 @@ class CachingImageManager : public QObject { Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) Q_PROPERTY(bool animated READ animated NOTIFY animatedChanged) + Q_PROPERTY(bool preferAnimated READ preferAnimated WRITE setPreferAnimated NOTIFY preferAnimatedChanged) public: explicit CachingImageManager(QObject* parent = nullptr) @@ -36,6 +37,8 @@ class CachingImageManager : public QObject { [[nodiscard]] QUrl cachePath() const; [[nodiscard]] bool animated() const { return m_animated; } + [[nodiscard]] bool preferAnimated() const { return m_preferAnimated; } + void setPreferAnimated(bool preferAnimated); Q_INVOKABLE void updateSource(); Q_INVOKABLE void updateSource(const QString& path); @@ -48,6 +51,7 @@ class CachingImageManager : public QObject { void cachePathChanged(); void usingCacheChanged(); void animatedChanged(); + void preferAnimatedChanged(); private: QString m_shaPath; @@ -59,6 +63,7 @@ class CachingImageManager : public QObject { QUrl m_cachePath; bool m_animated; + bool m_preferAnimated = true; QMetaObject::Connection m_widthConn; QMetaObject::Connection m_heightConn; From 8d9559056f00a1f284c5e685f8575dc76541f9e8 Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Fri, 7 Nov 2025 00:14:11 -0500 Subject: [PATCH 5/6] revert unrelated brightness changes (oops) --- services/Brightness.qml | 51 ++++++++++++----------------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index 0038b6060..ac905fd8e 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -78,18 +78,10 @@ Singleton { command: ["ddcutil", "detect", "--brief"] stdout: StdioCollector { - onStreamFinished: { - const blocks = text.trim().split("\n\n").filter(d => d.startsWith("Display ")); - root.ddcMonitors = blocks.map(d => { - const busMatch = d.match(/I2C bus:\s*\/dev\/i2c-([0-9]+)/i); - // Accept both "DRM connector:" and "DRM_connector:" - const connMatch = d.match(/DRM[_ ]connector:\s+(.*)/i); - return { - busNum: busMatch ? busMatch[1] : "", - connector: connMatch ? connMatch[1].replace(/^card\d+-/, "") : "" - }; - }).filter(m => m.busNum && m.connector); - } + onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ + busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], + connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" + })) } } @@ -167,27 +159,16 @@ Singleton { readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") property real brightness property real queuedBrightness: NaN - // Default to 250 for your Dell AW3423DW; override with parsed max if available. - property int vcpMax: 250 readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { - // Extract integers defensively; works for ddcutil and brightnessctl echoes - const nums = (text.match(/\d+/g) ?? []).map(n => parseInt(n, 10)).filter(n => !isNaN(n)); if (monitor.isAppleDisplay) { - const val = nums.at(-1) ?? 0; - monitor.vcpMax = 100; // Apple path writes 0..100 - monitor.brightness = val / 101; // keep original behavior - } else if (monitor.isDdc) { + const val = parseInt(text.trim()); + monitor.brightness = val / 101; + } else { const [, , , cur, max] = text.split(" "); monitor.brightness = parseInt(cur) / parseInt(max); - } else { - // brightnessctl path: our echo prints ... at the end - const cur = nums.at(-2) ?? 0; - const max = nums.at(-1) ?? 100; - monitor.vcpMax = 100; // writes use % for brightnessctl - monitor.brightness = max > 0 ? cur / max : 0; } } } @@ -205,8 +186,8 @@ Singleton { function setBrightness(value: real): void { value = Math.max(0, Math.min(1, value)); - const scaled100 = Math.round(value * 100); - if (Math.round((brightness ?? 0) * 100) === scaled100) + const rounded = Math.round(value * 100); + if (Math.round(brightness * 100) === rounded) return; if (isDdc && timer.running) { @@ -216,14 +197,12 @@ Singleton { brightness = value; - if (isAppleDisplay) { - Quickshell.execDetached(["asdbctl", "set", Math.round(value * 100)]); - } else if (isDdc) { - // Write using vcpMax (defaults to 250; uses parsed max when available) - Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", Math.round(value * vcpMax)]); - } else { - Quickshell.execDetached(["brightnessctl", "s", `${scaled100}%`]); - } + if (isAppleDisplay) + Quickshell.execDetached(["asdbctl", "set", rounded]); + else if (isDdc) + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]); + else + Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); if (isDdc) timer.restart(); From 355b2a871f51d775d4b8e2dbf877ea9b712586ec Mon Sep 17 00:00:00 2001 From: Evence Wang Date: Fri, 7 Nov 2025 15:22:46 -0500 Subject: [PATCH 6/6] lazy load preview on selection + stop playback on lock --- components/images/CachingImage.qml | 13 +++++++++++++ modules/background/Background.qml | 5 +++++ modules/background/Wallpaper.qml | 27 +++++++++++++++++++++++---- shell.qml | 8 +++++--- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index 0d4b69f54..8526c16e5 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -56,6 +56,19 @@ Item { } } + Image { + id: animatedPlaceholder + + anchors.fill: parent + asynchronous: root.asynchronous + cache: false + fillMode: root.fillMode + smooth: root.smooth + visible: manager.animated && root.preferAnimated && root.source && (!root.contentItem || root.contentItem.status !== Image.Ready) + source: root.source + sourceSize: root.sourceSize + } + Loader { id: loader diff --git a/modules/background/Background.qml b/modules/background/Background.qml index fbacfabb8..9ba2e7369 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -9,9 +9,13 @@ import Quickshell.Wayland import QtQuick Loader { + id: backgroundLoader + asynchronous: true active: Config.background.enabled + property var lock + sourceComponent: Variants { model: Quickshell.screens @@ -33,6 +37,7 @@ Loader { Wallpaper { id: wallpaper + sessionLock: backgroundLoader.lock ? backgroundLoader.lock.lock : null } Visualiser { diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 489deb9ef..ae87401c0 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -14,6 +14,8 @@ Item { property string source: Wallpapers.current property CachingImage current: one readonly property Item imageItem: (current && current.contentItem) ? current.contentItem : null + property var sessionLock: null + readonly property bool sessionLocked: sessionLock ? sessionLock.secure : false anchors.fill: parent @@ -113,22 +115,39 @@ Item { id: img function update(): void { - if (path === root.source) + if (!root.source) return; + + if (path === root.source) { root.current = this; - else - path = root.source; + return; + } + + const target = root.source; + path = target; + + if (img.animated) { + Qt.callLater(() => { + if (img.path === target && root.source === target) + root.current = img; + }); + } } anchors.fill: parent opacity: 0 scale: Wallpapers.showPreview ? 1 : 0.8 - playbackEnabled: root.current === img + playbackEnabled: root.current === img && !root.sessionLocked onStatusChanged: { if (status === Image.Ready) root.current = this; } + onPlaybackEnabledChanged: { + if (root.current === img && img.animated && playbackEnabled) + img.restart(); + } + Connections { target: root diff --git a/shell.qml b/shell.qml index 3ce777699..160272c81 100644 --- a/shell.qml +++ b/shell.qml @@ -10,12 +10,14 @@ import "modules/lock" import Quickshell ShellRoot { - Background {} - Drawers {} - AreaPicker {} Lock { id: lock } + Background { + lock: lock + } + Drawers {} + AreaPicker {} Shortcuts {} BatteryMonitor {}