import qs.config import qs.custom import qs.services import qs.util import QtQuick import QtQuick.Shapes Item { id: root anchors.fill: parent property real playerProgress: { const active = Players.active; return active?.length ? active.position / active.length : 0; } Behavior on playerProgress { Anim { duration: Config.anim.durations.large } } Timer { running: Players.active?.isPlaying ?? false interval: 400 triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() } Shape { id: progress preferredRendererType: Shape.CurveRenderer readonly property int thickness: 8 readonly property int angle: 300 ShapePath { id: path fillColor: "transparent" strokeColor: Qt.alpha(Config.colors.media, 0.2) strokeWidth: progress.thickness capStyle: ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + progress.thickness) / 2 + 7 radiusY: (cover.height + progress.thickness) / 2 + 7 startAngle: -90 - progress.angle / 2 sweepAngle: progress.angle } Behavior on strokeColor { CAnim {} } } ShapePath { fillColor: "transparent" strokeColor: Config.colors.media strokeWidth: progress.thickness capStyle: ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + progress.thickness) / 2 + 7 radiusY: (cover.height + progress.thickness) / 2 + 7 startAngle: -90 - progress.angle / 2 // NOTE: Cap progress angle to account for bad MPRIS players sweepAngle: progress.angle * Math.min(root.playerProgress, 1) } Behavior on strokeColor { CAnim {} } } } CustomClippingRect { id: cover anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 18 + progress.thickness implicitHeight: width color: Config.colors.inactive radius: Infinity MaterialIcon { anchors.centerIn: parent grade: 200 text: "art_track" color: Config.colors.tertiary font.pointSize: (parent.width * 0.4) || 1 } Image { id: image anchors.fill: parent source: Players.active?.trackArtUrl ?? "" asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height } } CustomText { id: title anchors.top: cover.bottom anchors.left: parent.left anchors.right: parent.right anchors.topMargin: album.text === "" ? 24 : 18 anchors.leftMargin: 15 anchors.rightMargin: 15 animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Config.colors.secondary font.pointSize: Config.font.size.normal elide: Text.ElideRight } CustomText { id: artist anchors.top: title.bottom anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: 15 anchors.rightMargin: 15 animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") opacity: Players.active ? 1 : 0 color: Config.colors.primary elide: Text.ElideRight } CustomText { id: album anchors.top: artist.bottom anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: 15 anchors.rightMargin: 15 animate: true horizontalAlignment: Text.AlignHCenter text: Players.active?.trackAlbum ?? "" opacity: Players.active ? 1 : 0 color: Config.colors.tertiary font.pointSize: Config.font.size.small elide: Text.ElideRight } Row { id: controls anchors.top: album.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: album.text === "" ? -4 : 2 spacing: 7 Control { icon: "skip_previous" canUse: Players.active?.canGoPrevious ?? false function onClicked(): void { Players.active?.previous(); } } Control { icon: Players.active?.isPlaying ? "pause" : "play_arrow" canUse: Players.active?.canTogglePlaying ?? false function onClicked(): void { Players.active?.togglePlaying(); } } Control { icon: "skip_next" canUse: Players.active?.canGoNext ?? false function onClicked(): void { Players.active?.next(); } } } component Control: CustomRect { id: control required property string icon required property bool canUse function onClicked(): void { } implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + 12 implicitHeight: implicitWidth StateLayer { anchors.fill: parent disabled: !control.canUse radius: 1000 function onClicked(): void { control.onClicked(); } } MaterialIcon { id: icon anchors.centerIn: parent anchors.verticalCenterOffset: font.pointSize * 0.05 animate: true text: control.icon color: control.canUse ? Config.colors.media : Config.colors.inactive font.pointSize: Config.font.size.large } } }