From 9cd9d7facdf78d7b7296df1628ea096f2a348dcd Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:03:56 +1000 Subject: [PATCH] feat: add anim for dash media cover --- components/controls/LoadingIndicator.qml | 79 ++++++++++++++++++++++++ modules/dashboard/dash/Media.qml | 74 +++++++++++++++++++++- 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 components/controls/LoadingIndicator.qml diff --git a/components/controls/LoadingIndicator.qml b/components/controls/LoadingIndicator.qml new file mode 100644 index 00000000..a98e0ff1 --- /dev/null +++ b/components/controls/LoadingIndicator.qml @@ -0,0 +1,79 @@ +import QtQuick +import M3Shapes +import Caelestia.Config +import qs.components +import qs.services + +MaterialShape { + id: root + + property list shapes: { + if (containsIcon) + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + } + property int shapeIndex + property real cRotation + property real lRotation + property bool containsIcon + + property bool animated: true + property int morphAnimRotation: 60 + property alias morphAnimDelay: morphAnimDelay.duration + property alias rotateAnimDuration: rotateAnim.duration + + readonly property easingCurve scaleCurve: [0.00, 0.56, 0.55, 1.29, 1, 1] + + implicitSize: 38 + rotation: cRotation + lRotation + + shape: shapes[shapeIndex] + color: Colours.palette.m3primary + + animationDuration: Tokens.anim.durations.expressiveSlowSpatial + animationEasing: Tokens.anim.expressiveSlowSpatial + + scale: { + const t = scaleCurve.valueForProgress(morphProgress); + return 1 + 0.15 * Math.sin(t * Math.PI) / 2; + } + + SequentialAnimation { + running: root.animated + loops: Animation.Infinite + + PropertyAction { + target: root + property: "shapeIndex" + value: (root.shapeIndex + 1) % root.shapes.length + } + RotationAnimation { + target: root + property: "lRotation" + to: (root.lRotation + root.morphAnimRotation) % 360 + duration: Tokens.anim.durations.expressiveSlowSpatial + easing: Tokens.anim.expressiveSlowSpatial + direction: RotationAnimation.Shortest + } + PauseAnimation { + id: morphAnimDelay + + duration: 20 + } + } + + RotationAnimation on cRotation { + id: rotateAnim + + running: root.animated + from: 0 + to: 360 + easing.type: Easing.Linear + loops: Animation.Infinite + duration: 10000 + } + + Behavior on color { + CAnim {} + } +} diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index f4b1fae3..f1486b92 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -20,6 +20,7 @@ Item { } readonly property real arcCoverGap: Tokens.spacing.extraSmall + property bool coverHadPrevious anchors.top: parent.top anchors.bottom: parent.bottom @@ -110,9 +111,35 @@ Item { anchors.centerIn: parent grade: 200 - text: "art_track" + text: image.status === Image.Error ? "broken_image" : "art_track" color: Colours.palette.m3onSurfaceVariant - fontStyle: Tokens.font.icon.size((parent.width * 0.4) || 1).build() + 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 { @@ -130,6 +157,49 @@ Item { layer.effect: Mask { maskSource: coverShape } + + 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(); + } + } + } + } + } } }