nosignal-shell/modules/dashboard/media/LyricList.qml
2 * r + 2 * t ac24024c03 fix: enable auto padding for lyric glow
Still slightly clipped on the left (due to list view) but shouldn't be
noticeable
2026-05-05 02:01:13 +10:00

296 lines
8 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
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
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: Lyrics.lyrics
Component.onCompleted: {
currentIndex = Qt.binding(() => {
Lyrics.lyrics; // 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
property real effectScale: ListView.isCurrentItem ? 1 : 0
anchors.left: ListView.view?.contentItem.left
anchors.right: ListView.view?.contentItem.right
text: modelData || ". . ."
color: ListView.isCurrentItem ? Colours.palette.m3primary : 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
}
}
}
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}