import QtQuick import Quickshell.Services.Mpris import qs.Common import qs.Services import qs.Widgets Item { id: widget readonly property var activePlayer: MprisController.activePlayer readonly property var players: MprisController.availablePlayers || [] readonly property bool hasPlayer: activePlayer !== null && activePlayer !== undefined readonly property bool isPlaying: hasPlayer && activePlayer.playbackState === MprisPlaybackState.Playing function formatTime(seconds) { const total = Math.max(0, Math.floor(seconds || 0)); const mins = Math.floor(total / 60); const secs = total % 60; return mins + ":" + (secs < 10 ? "0" : "") + secs; } function playerName(player) { if (!player) return ""; return player.identity || player.dbusName || "Player"; } Item { id: content anchors.fill: parent visible: widget.hasPlayer DankAlbumArt { id: art anchors.left: parent.left anchors.top: parent.top width: 84 height: 84 activePlayer: widget.activePlayer showAnimation: widget.isPlaying } Column { id: meta anchors.left: art.right anchors.leftMargin: Theme.spacingM anchors.right: parent.right anchors.top: parent.top anchors.topMargin: Theme.spacingXS spacing: 2 StyledText { width: parent.width text: (widget.activePlayer && widget.activePlayer.trackTitle) ? widget.activePlayer.trackTitle : "Nothing playing" font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText elide: Text.ElideRight } StyledText { width: parent.width text: (widget.activePlayer && widget.activePlayer.trackArtist) ? widget.activePlayer.trackArtist : "" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText elide: Text.ElideRight visible: text !== "" } StyledText { width: parent.width text: (widget.activePlayer && widget.activePlayer.trackAlbum) ? widget.activePlayer.trackAlbum : "" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText elide: Text.ElideRight visible: text !== "" } } DankSeekbar { id: seekbar anchors.left: parent.left anchors.right: parent.right anchors.top: art.bottom anchors.topMargin: Theme.spacingS height: 20 activePlayer: widget.activePlayer } StyledText { id: positionLabel anchors.left: parent.left anchors.top: seekbar.bottom text: widget.formatTime(widget.activePlayer ? widget.activePlayer.position : 0) font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } StyledText { anchors.right: parent.right anchors.top: seekbar.bottom text: widget.formatTime(MprisController.activePlayerStableLength) font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText visible: MprisController.activePlayerStableLength > 0 } Row { id: transport anchors.horizontalCenter: parent.horizontalCenter anchors.top: positionLabel.bottom anchors.topMargin: Theme.spacingXS spacing: Theme.spacingM DankActionButton { anchors.verticalCenter: parent.verticalCenter iconName: "skip_previous" buttonSize: 38 iconSize: 22 iconColor: Theme.surfaceText enabled: widget.hasPlayer && widget.activePlayer.canGoPrevious onClicked: MprisController.previousOrRewind() } Rectangle { anchors.verticalCenter: parent.verticalCenter width: 46 height: 46 radius: width / 2 color: Theme.primary opacity: (widget.hasPlayer && widget.activePlayer.canTogglePlaying) ? 1 : 0.5 DankIcon { anchors.centerIn: parent name: widget.isPlaying ? "pause" : "play_arrow" size: 26 color: Theme.onPrimary weight: 500 } MouseArea { anchors.fill: parent enabled: widget.hasPlayer && widget.activePlayer.canTogglePlaying cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: widget.activePlayer.togglePlaying() } } DankActionButton { anchors.verticalCenter: parent.verticalCenter iconName: "skip_next" buttonSize: 38 iconSize: 22 iconColor: Theme.surfaceText enabled: widget.hasPlayer && widget.activePlayer.canGoNext onClicked: MprisController.next() } } // Source switcher - one chip per MPRIS player that is currently around. DankFlickable { id: sourceRow anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 30 clip: true contentHeight: height contentWidth: sourceChips.width flickableDirection: Flickable.HorizontalFlick visible: widget.players.length > 1 Row { id: sourceChips height: sourceRow.height spacing: Theme.spacingXS Repeater { model: widget.players delegate: Rectangle { id: chip required property var modelData readonly property bool current: modelData === widget.activePlayer width: chipLabel.implicitWidth + Theme.spacingM * 2 height: 26 anchors.verticalCenter: parent.verticalCenter radius: height / 2 color: current ? Theme.primarySelected : (chipArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)) border.color: current ? Theme.primary : Theme.outlineLight border.width: current ? 1 : Theme.layerOutlineWidth StyledText { id: chipLabel anchors.centerIn: parent text: widget.playerName(chip.modelData) font.pixelSize: Theme.fontSizeSmall font.weight: chip.current ? Font.Medium : Font.Normal color: chip.current ? Theme.primary : Theme.surfaceText } MouseArea { id: chipArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: MprisController.setActivePlayer(chip.modelData) } } } } } } Column { anchors.centerIn: parent spacing: Theme.spacingS visible: !widget.hasPlayer DankIcon { anchors.horizontalCenter: parent.horizontalCenter name: "music_off" size: Theme.iconSizeLarge color: Theme.surfaceVariantText } StyledText { anchors.horizontalCenter: parent.horizontalCenter text: "No media player running" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } } }