import QtQuick import QtQuick.Shapes import Caelestia.Services import qs.components import qs.services import Caelestia.Config import qs.utils Item { id: root property real playerProgress: { const active = Players.active; return active?.length ? active.position / active.length : 0; } anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Tokens.sizes.dashboard.mediaWidth Behavior on playerProgress { Anim { duration: Tokens.anim.durations.large } } Timer { running: Players.active?.isPlaying ?? false interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() } ServiceRef { service: Audio.beatTracker } Shape { preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep } Behavior on strokeColor { CAnim {} } } ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { CAnim {} } } } StyledClippingRect { id: cover anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small implicitHeight: width color: Colours.tPalette.m3surfaceContainerHigh radius: Infinity MaterialIcon { anchors.centerIn: parent grade: 200 text: "art_track" color: Colours.palette.m3onSurfaceVariant font.pointSize: (parent.width * 0.4) || 1 } Image { id: image anchors.fill: parent source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height } } StyledText { id: title anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Tokens.spacing.normal animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary font.pointSize: Tokens.font.size.normal width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } StyledText { id: album anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline font.pointSize: Tokens.font.size.small width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } StyledText { id: artist anchors.top: album.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } Row { id: controls anchors.top: artist.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Tokens.spacing.smaller spacing: Tokens.spacing.small Control { function onClicked(): void { Players.active?.previous(); } icon: "skip_previous" canUse: Players.active?.canGoPrevious ?? false } Control { function onClicked(): void { Players.active?.togglePlaying(); } icon: Players.active?.isPlaying ? "pause" : "play_arrow" canUse: Players.active?.canTogglePlaying ?? false } Control { function onClicked(): void { Players.active?.next(); } icon: "skip_next" canUse: Players.active?.canGoNext ?? false } } AnimatedImage { id: bongocat anchors.top: controls.bottom anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.topMargin: Tokens.spacing.small anchors.bottomMargin: Tokens.padding.large anchors.margins: Tokens.padding.large * 2 playing: Players.active?.isPlaying ?? false speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } component Control: StyledRect { id: control required property string icon required property bool canUse function onClicked(): void { } implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small implicitHeight: implicitWidth StateLayer { function onClicked(): void { control.onClicked(); } disabled: !control.canUse radius: Tokens.rounding.full } MaterialIcon { id: icon anchors.centerIn: parent anchors.verticalCenterOffset: font.pointSize * 0.05 animate: true text: control.icon color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline font.pointSize: Tokens.font.size.large } } }