pragma ComponentBehavior: Bound import qs.config import qs.custom import qs.services import qs.util import Quickshell import Quickshell.Widgets import Quickshell.Services.Mpris import QtQuick import QtQuick.Layouts import QtQuick.Shapes Item { id: root required property PersistentProperties uiState property real playerProgress: { const active = Players.active; return active?.length ? active.position / active.length : 0; } function lengthStr(length: int): string { if (length < 0) return "-1:-1"; const hours = Math.floor(length / 3600); const mins = Math.floor((length % 3600) / 60); const secs = Math.floor(length % 60).toString().padStart(2, "0"); if (hours > 0) return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; return `${mins}:${secs}`; } implicitWidth: slider.width + 32 implicitHeight: childrenRect.height Timer { running: Players.active?.isPlaying ?? false interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() } Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: Config.dashboard.mediaCoverArtHeight CustomClippingRect { id: cover anchors.horizontalCenter: parent.horizontalCenter anchors.leftMargin: 12 anchors.rightMargin: 12 anchors.verticalCenter: parent.top anchors.verticalCenterOffset: Config.dashboard.mediaCoverArtHeight / 2 implicitWidth: image.paintedWidth > 0 ? image.paintedWidth : Config.dashboard.mediaCoverArtHeight implicitHeight: image.paintedHeight > 0 ? image.paintedHeight : Config.dashboard.mediaCoverArtHeight color: Config.colors.containerDash radius: 12 MaterialIcon { anchors.centerIn: parent grade: 200 text: "art_track" color: Config.colors.tertiary font.pointSize: (parent.width * 0.4) || 1 } Image { id: image anchors.centerIn: parent width: Config.dashboard.mediaCoverArtWidth height: Config.dashboard.mediaCoverArtHeight source: Players.active?.trackArtUrl ?? "" asynchronous: true fillMode: Image.PreserveAspectFit sourceSize.width: width sourceSize.height: height } } } ColumnLayout { id: details anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 12 + Config.dashboard.mediaCoverArtHeight spacing: 8 CustomText { id: title Layout.fillWidth: true Layout.maximumWidth: parent.implicitWidth 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 wrapMode: Text.WrapAtWordBoundaryOrAnywhere } CustomText { id: artist Layout.fillWidth: true visible: Players.active animate: true horizontalAlignment: Text.AlignHCenter text: Players.active?.trackArtist || qsTr("Unknown artist") opacity: Players.active ? 1 : 0 color: Config.colors.primary wrapMode: Text.WrapAtWordBoundaryOrAnywhere } CustomText { id: album Layout.fillWidth: true Layout.topMargin: -5 visible: text !== "" animate: true horizontalAlignment: Text.AlignHCenter text: Players.active?.trackAlbum ?? "" color: Config.colors.tertiary wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { id: controls Layout.alignment: Qt.AlignHCenter Layout.topMargin: 12 Layout.bottomMargin: 10 spacing: 7 PlayerControl { icon.text: "skip_previous" canUse: Players.active?.canGoPrevious ?? false function onClicked(): void { Players.active?.previous(); } } CustomRect { id: playBtn implicitWidth: Math.max(playIcon.implicitWidth, playIcon.implicitHeight) implicitHeight: implicitWidth radius: Players.active?.isPlaying ? 12 : implicitHeight / 2 color: { if (!Players.active?.canTogglePlaying) return "transparent"; return Players.active?.isPlaying ? Config.colors.media : Config.colors.container; } StateLayer { anchors.fill: parent disabled: !Players.active?.canTogglePlaying color: Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.primary radius: parent.radius function onClicked(): void { Players.active?.togglePlaying(); } } MaterialIcon { id: playIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -font.pointSize * 0.02 anchors.verticalCenterOffset: font.pointSize * 0.02 animate: true fill: 1 text: Players.active?.isPlaying ? "pause" : "play_arrow" color: { if (!Players.active?.canTogglePlaying) return Config.colors.inactive; return Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.media; } font.pointSize: Config.font.size.largest } Behavior on radius { Anim {} } } PlayerControl { icon.text: "skip_next" canUse: Players.active?.canGoNext ?? false function onClicked(): void { Players.active?.next(); } } } CustomSlider { id: slider enabled: !!Players.active progressColor: Config.colors.media implicitWidth: controls.implicitWidth * 1.5 implicitHeight: 20 onMoved: { const active = Players.active; if (active?.canSeek && active?.positionSupported) active.position = value * active.length; } Binding { target: slider property: "value" value: root.playerProgress when: !slider.pressed } CustomMouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) return; event.accepted = true; const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds Qt.callLater(() => { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } } } Item { Layout.fillWidth: true Layout.topMargin: -6 implicitHeight: Math.max(position.implicitHeight, length.implicitHeight) opacity: Players.active ? 1 : 0 visible: opacity > 0 CustomText { id: position anchors.left: parent.left text: root.lengthStr(Players.active?.position ?? 0) color: Config.colors.primary font.pointSize: Config.font.size.small } CustomText { id: length anchors.right: parent.right text: root.lengthStr(Players.active?.length ?? 0) color: Config.colors.primary font.pointSize: Config.font.size.small } Behavior on opacity { Anim { duration: Config.anim.durations.small } } } RowLayout { Layout.alignment: Qt.AlignHCenter Layout.topMargin: 12 spacing: 7 PlayerControl { icon.text: "flip_to_front" canUse: Players.active?.canRaise ?? false fontSize: Config.font.size.larger size: playerSelector.height fill: 0 color: Config.colors.container activeColor: Config.colors.secondary function onClicked(): void { Players.active?.raise(); root.uiState.dashboard = false; } } CustomRect { id: playerSelector property bool expanded Layout.alignment: Qt.AlignVCenter implicitWidth: slider.implicitWidth * 0.6 implicitHeight: currentPlayer.implicitHeight + 14 radius: 17 color: Config.colors.container z: 1 StateLayer { anchors.fill: parent disabled: Players.list.length <= 1 function onClicked(): void { playerSelector.expanded = !playerSelector.expanded; } } RowLayout { id: currentPlayer anchors.centerIn: parent spacing: 7 PlayerIcon { player: Players.active } CustomText { Layout.fillWidth: true Layout.maximumWidth: playerSelector.implicitWidth - implicitHeight - parent.spacing - 20 text: Players.active ? Players.getName(Players.active) : qsTr("No players") color: Players.active ? Config.colors.primary : Config.colors.tertiary elide: Text.ElideRight } } Elevation { anchors.fill: playerSelectorBg radius: playerSelectorBg.radius opacity: playerSelector.expanded ? 1 : 0 level: 2 Behavior on opacity { Anim { duration: Config.anim.durations.expressiveDefaultSpatial } } } CustomClippingRect { id: playerSelectorBg anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom implicitWidth: playerSelector.expanded ? playerList.implicitWidth : playerSelector.implicitWidth implicitHeight: playerSelector.expanded ? playerList.implicitHeight : playerSelector.implicitHeight color: Config.colors.containerAlt radius: 17 opacity: playerSelector.expanded ? 1 : 0 ColumnLayout { id: playerList anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom spacing: 0 Repeater { model: [...Players.list].sort((a, b) => (a === Players.active) - (b === Players.active)) Item { id: player required property MprisPlayer modelData Layout.fillWidth: true Layout.minimumWidth: playerSelector.implicitWidth implicitWidth: playerInner.implicitWidth + 20 implicitHeight: playerInner.implicitHeight + 14 StateLayer { disabled: !playerSelector.expanded function onClicked(): void { playerSelector.expanded = false; Players.manualActive = player.modelData; } } RowLayout { id: playerInner anchors.centerIn: parent spacing: 7 PlayerIcon { player: player.modelData } CustomText { text: Players.getName(player.modelData) color: Config.colors.primary } } } } } Behavior on opacity { Anim { duration: Config.anim.durations.expressiveDefaultSpatial } } Behavior on implicitWidth { Anim { duration: Config.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { duration: Config.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial } } } } PlayerControl { icon.text: "close" icon.anchors.horizontalCenterOffset: 0 canUse: Players.active?.canQuit ?? false size: playerSelector.height fontSize: Config.font.size.larger fill: 1 color: Config.colors.container activeColor: Config.colors.error function onClicked(): void { Players.active?.quit(); } } } } component PlayerIcon: Loader { id: loader required property MprisPlayer player readonly property string icon: player ? Icons.getAppIcon(Players.getIdentity(player)) : "" Layout.fillHeight: true sourceComponent: !player || icon === "image://icon/" ? fallbackIcon : playerImage Component { id: playerImage IconImage { implicitWidth: height source: loader.icon } } Component { id: fallbackIcon MaterialIcon { text: loader.player ? "animated_images" : "music_off" } } } component PlayerControl: CustomRect { id: control required property bool canUse property alias icon: icon property int fontSize: Config.font.size.largest property int size: Math.max(icon.implicitWidth, icon.implicitHeight) property real fill: 1 property color activeColor: Config.colors.media function onClicked() {} implicitWidth: size implicitHeight: implicitWidth radius: 1000 StateLayer { anchors.fill: parent disabled: !control.canUse function onClicked(): void { control.onClicked(); } } MaterialIcon { id: icon anchors.centerIn: parent anchors.horizontalCenterOffset: -font.pointSize * 0.02 anchors.verticalCenterOffset: font.pointSize * 0.02 animate: true fill: control.fill text: control.icon color: control.canUse ? control.activeColor : Config.colors.inactive font.pointSize: control.fontSize } } }