pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import QtQuick.Layouts import Caelestia.Config import Caelestia.Services import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services Item { id: root // Funny binding hack to make lyrics update readonly property var _: { const p = Players.active; if (p) Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length); else Lyrics.clearTrack(); } readonly property real fadeAmount: 0.1 property bool flag property list lyricList: Lyrics.lyrics layer.enabled: true layer.effect: Mask { maskSource: mask Rectangle { id: mask layer.enabled: true visible: false implicitWidth: root.width implicitHeight: root.height gradient: Gradient { orientation: Gradient.Vertical GradientStop { color: Qt.alpha("black", 0) position: 0 } GradientStop { color: Qt.alpha("black", 1) position: root.fadeAmount } GradientStop { color: Qt.alpha("black", 1) position: 1 - root.fadeAmount } GradientStop { color: Qt.alpha("black", 0) position: 1 } } } } state: { flag; // For some reason it doesn't update sometimes, so use this to force an update if (Lyrics.hasLyrics) return "hasLyrics"; if (Lyrics.loading) return "loading"; return "noLyrics"; } states: [ State { name: "loading" PropertyChanges { loadingIndicator.opacity: 1 lyrics.opacity: 0 noLyrics.opacity: 0 } }, State { name: "hasLyrics" PropertyChanges { loadingIndicator.opacity: 0 lyrics.opacity: 1 noLyrics.opacity: 0 } }, State { name: "noLyrics" PropertyChanges { loadingIndicator.opacity: 0 lyrics.opacity: 0 noLyrics.opacity: 1 } } ] transitions: [ Transition { from: "loading" SequentialAnimation { Anim { target: loadingIndicator property: "opacity" type: Anim.DefaultEffects } Anim { targets: [lyrics, noLyrics] property: "opacity" type: Anim.SlowEffects } } }, Transition { from: "hasLyrics" SequentialAnimation { Anim { target: lyrics property: "opacity" type: Anim.DefaultEffects } Anim { targets: [loadingIndicator, noLyrics] property: "opacity" type: Anim.SlowEffects } } }, Transition { from: "noLyrics" SequentialAnimation { Anim { target: noLyrics property: "opacity" type: Anim.DefaultEffects } Anim { targets: [loadingIndicator, lyrics] property: "opacity" type: Anim.SlowEffects } } } ] Connections { function onHasLyricsChanged() { root.flag = !root.flag; } target: Lyrics } Loader { id: loadingIndicator anchors.centerIn: parent asynchronous: true active: opacity > 0 opacity: 0 sourceComponent: ColumnLayout { spacing: Tokens.spacing.large StyledRect { Layout.alignment: Qt.AlignHCenter implicitWidth: shape.implicitSize + Tokens.padding.medium * 2 implicitHeight: shape.implicitSize + Tokens.padding.medium * 2 color: Colours.palette.m3primaryContainer radius: Tokens.rounding.full LoadingIndicator { id: shape anchors.centerIn: parent implicitSize: Math.round(Tokens.sizes.dashboard.mediaSectionWidth / 5) containsIcon: true // This removes the pentagon, which is not centered } } StyledText { text: qsTr("Loading lyrics...") color: Colours.palette.m3onSurfaceVariant font: Tokens.font.title.medium } } Behavior on opacity { Anim { type: Anim.DefaultEffects } } } Loader { id: noLyrics anchors.centerIn: parent asynchronous: true active: opacity > 0 opacity: 0 sourceComponent: ColumnLayout { spacing: Tokens.spacing.small MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "sentiment_sad" fontStyle: Tokens.font.icon.builders.large.scale(2).build() color: Colours.palette.m3outline } StyledText { text: qsTr("No lyrics found") color: Colours.palette.m3outline font: Tokens.font.title.medium } } } StyledListView { id: lyrics anchors.fill: parent anchors.topMargin: parent.height * root.fadeAmount / 2 anchors.bottomMargin: parent.height * root.fadeAmount / 2 displayMarginBeginning: anchors.topMargin displayMarginEnd: anchors.bottomMargin model: root.lyricList Component.onCompleted: { currentIndex = Qt.binding(() => { model; // Force update when lyrics change return Lyrics.indexForTime(Players.active?.position ?? 0); }); positionViewAtIndex(currentIndex, ListView.Center); } onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)) highlightRangeMode: ListView.ApplyRange highlightMoveDuration: Tokens.anim.durations.large highlightMoveVelocity: -1 preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2 preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2 spacing: Tokens.spacing.small opacity: 0 delegate: StyledText { id: lyric required property string modelData required property int index property real effectScale: ListView.isCurrentItem ? 1 : 0 anchors.left: lyrics.contentItem.left anchors.right: lyrics.contentItem.right text: modelData || ". . ." color: ListView.isCurrentItem ? Colours.palette.m3primary : mouse.containsMouse ? Colours.palette.m3onSurface : Colours.palette.m3outline font: Tokens.font.body.medium wrapMode: Text.WrapAtWordBoundaryOrAnywhere layer.enabled: effectScale > 0 layer.effect: MultiEffect { shadowEnabled: true shadowColor: Colours.palette.m3primary shadowOpacity: 0.5 * lyric.effectScale shadowBlur: 0.6 * lyric.effectScale blur: 0.4 * lyric.effectScale } Behavior on effectScale { Anim { type: Anim.SlowEffects } } MouseArea { id: mouse anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { const p = Players.active; if (p) p.position = Lyrics.timeForIndex(lyric.index); } } } Behavior on opacity { Anim { type: Anim.SlowEffects } } } Behavior on lyricList { SequentialAnimation { Anim { target: lyrics property: "opacity" to: 0 type: Anim.DefaultEffects } PropertyAction {} Anim { target: lyrics property: "opacity" to: 1 type: Anim.SlowEffects } } } }