refactor: move media cover art to separate component

This commit is contained in:
2 * r + 2 * t 2026-05-01 23:46:45 +10:00
parent 61738554e5
commit a54fe6f487
2 changed files with 163 additions and 148 deletions

View file

@ -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();
}
}
}
}
}
}
}

View file

@ -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 {