From a54fe6f487c0c179f1b331b6f827ca1483e3e72a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 1 May 2026 23:46:45 +1000 Subject: [PATCH] refactor: move media cover art to separate component --- components/widgets/CoverArt.qml | 161 +++++++++++++++++++++++++++++++ modules/dashboard/dash/Media.qml | 150 +--------------------------- 2 files changed, 163 insertions(+), 148 deletions(-) create mode 100644 components/widgets/CoverArt.qml diff --git a/components/widgets/CoverArt.qml b/components/widgets/CoverArt.qml new file mode 100644 index 00000000..c21b4708 --- /dev/null +++ b/components/widgets/CoverArt.qml @@ -0,0 +1,161 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import Quickshell +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services + +Item { + id: root + + readonly property alias shape: shape + + property bool hadPrevious + property color fallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Slight glow to separate from bg + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 1 + shadowColor: Colours.palette.m3outline + shadowOpacity: 0.3 + } + + Behavior on fallbackColour { + CAnim {} + } + + Item { + id: shapeWrapper + + anchors.fill: parent + layer.enabled: true + opacity: root.fallbackColour.a + + MaterialShape { + id: shape + + implicitSize: root.width + shape: MaterialShape.Cookie12Sided + color: Qt.alpha(root.fallbackColour, 1) + + Anim on rotation { + running: true + paused: !Players.active?.isPlaying + from: 360 + to: 0 + duration: 23500 + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Behavior on color { + CAnim {} + } + } + } + + MaterialIcon { + anchors.centerIn: parent + + grade: 200 + text: image.status === Image.Error ? "broken_image" : "art_track" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build() + opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0 + animate: true + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: image.status === Image.Loading ? 1 : 0 + + sourceComponent: LoadingIndicator { + implicitSize: root.width * 0.3 + color: Colours.palette.m3primaryContainer + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Image { + id: image + + anchors.fill: parent + + source: Players.getArtUrl(Players.active) + asynchronous: true + fillMode: Image.PreserveAspectCrop + + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + layer.enabled: true + layer.effect: Mask { + maskSource: shapeWrapper + } + + retainWhileLoading: true + opacity: 0 + + onStatusChanged: { + if (!opacityInAnim.running && image.status === Image.Ready) { + opacityInAnim.type = root.hadPrevious ? Anim.DefaultEffects : Anim.StandardLarge; + opacityInAnim.start(); + } + } + + Anim on opacity { + id: opacityInAnim + + running: false + to: 1 + type: Anim.DefaultEffects + } + + Behavior on source { + SequentialAnimation { + Anim { + target: image + property: "opacity" + to: 0 + type: Anim.FastEffects + } + PropertyAction { + target: root + property: "hadPrevious" + value: image.source + } + PropertyAction {} + ScriptAction { + script: { + if (!opacityInAnim.running && image.status === Image.Ready) { + opacityInAnim.type = root.hadPrevious ? Anim.DefaultEffects : Anim.StandardLarge; + opacityInAnim.start(); + } + } + } + } + } + } +} diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 0e123072..7b687a5b 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,14 +1,11 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects -import Quickshell -import M3Shapes import Caelestia.Config import Caelestia.Services import qs.components import qs.components.controls -import qs.components.effects +import qs.components.widgets import qs.services import qs.utils @@ -21,8 +18,6 @@ Item { } readonly property real arcCoverGap: Tokens.spacing.extraSmall - property bool coverHadPrevious - property color coverFallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) anchors.top: parent.top anchors.bottom: parent.bottom @@ -34,10 +29,6 @@ Item { } } - Behavior on coverFallbackColour { - CAnim {} - } - Timer { running: Players.active?.isPlaying ?? false interval: GlobalConfig.dashboard.mediaUpdateInterval @@ -68,7 +59,7 @@ Item { wavePaused: !Players.active?.isPlaying } - Item { + CoverArt { id: cover anchors.top: parent.top @@ -76,143 +67,6 @@ Item { anchors.right: parent.right anchors.margins: Tokens.padding.medium + root.arcCoverGap + prog.thickness implicitHeight: width - - // Slight glow to separate from bg - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 1 - shadowColor: Colours.palette.m3outline - shadowOpacity: 0.3 - } - - Item { - id: coverShapeWrapper - - anchors.fill: parent - layer.enabled: true - opacity: root.coverFallbackColour.a - - MaterialShape { - id: coverShape - - implicitSize: cover.width - shape: MaterialShape.Cookie12Sided - color: Qt.alpha(root.coverFallbackColour, 1) - - Anim on rotation { - running: true - paused: !Players.active?.isPlaying - from: 360 - to: 0 - duration: 23500 - easing.type: Easing.Linear - loops: Animation.Infinite - } - - Behavior on color { - CAnim {} - } - } - } - - MaterialIcon { - anchors.centerIn: parent - - grade: 200 - text: image.status === Image.Error ? "broken_image" : "art_track" - color: Colours.palette.m3onSurfaceVariant - fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build() - opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0 - animate: true - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - } - - Loader { - anchors.centerIn: parent - asynchronous: true - active: opacity > 0 - opacity: image.status === Image.Loading ? 1 : 0 - - sourceComponent: LoadingIndicator { - implicitSize: cover.width * 0.3 - color: Colours.palette.m3primaryContainer - } - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - } - - Image { - id: image - - anchors.fill: parent - - source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop - - sourceSize: { - const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; - return Qt.size(width * dpr, height * dpr); - } - - layer.enabled: true - layer.effect: Mask { - maskSource: coverShapeWrapper - } - - retainWhileLoading: true - opacity: 0 - - onStatusChanged: { - if (!opacityInAnim.running && image.status === Image.Ready) { - opacityInAnim.type = root.coverHadPrevious ? Anim.DefaultEffects : Anim.StandardLarge; - opacityInAnim.start(); - } - } - - Anim on opacity { - id: opacityInAnim - - running: false - to: 1 - type: Anim.DefaultEffects - } - - Behavior on source { - SequentialAnimation { - Anim { - target: image - property: "opacity" - to: 0 - type: Anim.FastEffects - } - PropertyAction { - target: root - property: "coverHadPrevious" - value: image.source - } - PropertyAction {} - ScriptAction { - script: { - if (!opacityInAnim.running && image.status === Image.Ready) { - opacityInAnim.type = root.coverHadPrevious ? Anim.DefaultEffects : Anim.StandardLarge; - opacityInAnim.start(); - } - } - } - } - } - } } StyledText {