pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import QtQuick.Layouts import ZShell.Config import ZShell.Services import qs.Components import qs.Effects import qs.Services import qs.Helpers 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 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: { 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 } } } ] Loader { id: loadingIndicator anchors.centerIn: parent asynchronous: true active: opacity > 0 opacity: 0 sourceComponent: ColumnLayout { spacing: Tokens.spacing.large CustomRect { Layout.alignment: Qt.AlignHCenter implicitWidth: shape.implicitSize + Tokens.padding.medium * 2 implicitHeight: shape.implicitSize + Tokens.padding.medium * 2 color: Colors.palette.m3primaryContainer radius: Tokens.rounding.full LoadingIndicator { id: shape anchors.centerIn: parent implicitSize: Math.round(300 / 5) containsIcon: true // This removes the pentagon, which is not centered } } CustomText { text: qsTr("Loading lyrics...") color: Colors.palette.m3onSurfaceVariant font: Tokens.font.body.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.extraLarge color: Colors.palette.m3outline } CustomText { text: qsTr("No lyrics found") color: Colors.palette.m3outline font: Tokens.font.body.medium } } } CustomListView { 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 enabled: opacity > 0 delegate: CustomText { 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 ? Colors.palette.m3primary : mouse.containsMouse ? Colors.palette.m3onSurface : Colors.palette.m3outline font: Tokens.font.body.medium wrapMode: Text.WrapAtWordBoundaryOrAnywhere layer.enabled: effectScale > 0 layer.effect: MultiEffect { shadowEnabled: true shadowColor: Colors.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 } } } }