repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/MediaPanel.qml
Rawpragma ComponentBehavior: Bound
import QtQuick
import "Util.js" as Util
Flickable {
id: root
required property Theme theme
required property Media mediaService
readonly property var panelState: mediaService ? Util.parseJson(mediaService.stateJson(), {}) : ({})
readonly property var player: panelState.activePlayer || ({})
readonly property var players: panelState.players || []
readonly property bool hasMedia: players.length > 0
readonly property var panel: panelState.mediaPanelContract || ({})
// The existing service snapshot omits transport capabilities. Read them from
// its native-player lookup, without introducing another command route.
readonly property var nativePlayer: mediaService && hasMedia ? mediaService.nativePlayerById(player.id) : null
readonly property bool canSeek: !!(nativePlayer && nativePlayer.canControl && player.canSeek && Number(player.length) > 0)
readonly property var transportActions: ["shuffle", "previous", "playPause", "next", "repeat"]
function mmss(seconds) {
const total = Math.max(0, Math.round(Number(seconds || 0)));
const minutes = Math.floor(total / 60);
const rest = String(total % 60).padStart(2, "0");
return `${minutes}:${rest}`;
}
function actionEnabled(kind) {
if (!nativePlayer || !nativePlayer.canControl)
return false;
if (kind === "shuffle")
return !!player.canShuffle;
if (kind === "repeat")
return !!player.canRepeat;
if (kind === "previous")
return !!nativePlayer.canGoPrevious;
if (kind === "next")
return !!nativePlayer.canGoNext;
if (kind === "playPause")
return !!nativePlayer.canTogglePlaying;
return false;
}
function runAction(kind) {
if (mediaService && actionEnabled(kind))
mediaService.action(kind);
}
function seekByRatio(ratio) {
const length = Math.max(0, Number(player.length || 0));
if (mediaService && canSeek)
mediaService.seek(Math.round(Math.max(0, Math.min(1, ratio)) * length));
}
function selectPlayer(id) {
if (mediaService)
mediaService.select(id);
}
function actionIcon(kind) {
if (kind === "shuffle")
return "\uF074";
if (kind === "previous")
return "";
if (kind === "playPause")
return root.player.playbackState === "playing" ? "\uF04C" : "\uF04B";
if (kind === "next")
return "";
if (kind === "repeat")
return "\uF01E";
return "\uF001";
}
function actionActive(kind) {
return kind === "shuffle" && root.player.shuffle || kind === "repeat" && root.player.repeat !== "none";
}
function actionLabel(kind) {
if (kind === "playPause")
return root.player.playbackState === "playing" ? "pause" : "play";
if (kind === "previous")
return "previous track";
if (kind === "next")
return "next track";
return kind;
}
width: parent ? parent.width : 0
contentWidth: width
contentHeight: mediaCol.height
clip: true
interactive: contentHeight > height
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
StableListModel {
id: playerModel
rows: root.players
}
Column {
id: mediaCol
width: root.width
spacing: 12
PopupCard {
id: nowPlayingCard
theme: root.theme
padding: 0
objectName: "coverDetailsProgressTransport"
visible: root.hasMedia
width: parent.width
height: 252
Rectangle {
id: coverCard
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: 12
width: 104
height: 104
radius: 12
color: root.theme.ui.backdrop
clip: true
Image {
id: coverArt
anchors.fill: parent
visible: !!root.player.artAvailable && status === Image.Ready
source: root.player.cover || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
}
Text {
anchors.centerIn: parent
visible: !coverArt.visible
font.family: "Symbols Nerd Font Mono"
font.pixelSize: 36
color: root.theme.ui.accent
text: "\uF001"
}
}
Column {
anchors.left: coverCard.right
anchors.leftMargin: 16
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: coverCard.verticalCenter
spacing: 6
NuguText {
theme: root.theme
variant: "title"
text: root.player.title || root.player.identity || "untitled"
width: parent.width
elide: Text.ElideRight
}
NuguText {
theme: root.theme
text: root.player.artist || "unknown artist"
width: parent.width
elide: Text.ElideRight
}
NuguText {
theme: root.theme
variant: "meta"
text: root.player.album || ""
visible: text.length > 0
width: parent.width
elide: Text.ElideRight
}
Row {
width: parent.width
spacing: 8
PopupBadge {
id: playbackBadge
theme: root.theme
text: root.player.playbackState || "stopped"
role: root.player.playbackState === "playing" ? "active" : "normal"
}
NuguText {
theme: root.theme
variant: "meta"
text: root.player.identity || "mpris"
width: parent.width - playbackBadge.width - 8
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
}
Item {
id: progressBlock
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 12
anchors.bottom: transportRow.top
anchors.bottomMargin: 12
height: 44
NuguText {
anchors.left: parent.left
anchors.top: parent.top
theme: root.theme
variant: "mono"
text: root.mmss(root.player.position)
}
NuguText {
anchors.right: parent.right
anchors.top: parent.top
theme: root.theme
variant: "mono"
text: root.mmss(root.player.length)
}
PopupSlider {
objectName: "mpris-progress"
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
theme: root.theme
enabled: root.canSeek
Accessible.name: `playback position for ${root.player.title || root.player.identity || "media"}`
value: Number(root.player.progress || 0)
onMoved: value => root.seekByRatio(value)
}
}
Row {
id: transportRow
objectName: "transportActions"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
spacing: 8
Repeater {
model: root.transportActions
delegate: PopupButton {
required property string modelData
readonly property bool isPrimary: modelData === "playPause"
theme: root.theme
width: 40
height: 34
icon: root.actionIcon(modelData)
accessibleName: root.actionLabel(modelData)
tooltip: enabled ? accessibleName : `${accessibleName} unavailable for this player`
enabled: root.actionEnabled(modelData)
role: isPrimary ? "primary" : "normal"
checked: root.actionActive(modelData)
onClicked: root.runAction(modelData)
}
}
}
}
Flow {
objectName: "playerSelectorRail"
width: parent.width
spacing: 6
visible: root.players.length > 1
Repeater {
model: playerModel
delegate: PopupButton {
required property var rowData
theme: root.theme
width: Math.min(root.width, implicitWidth)
text: rowData.identity || rowData.id
accessibleName: `select ${text}`
checked: rowData.id === root.player.id
onClicked: root.selectPlayer(rowData.id)
}
}
}
Item {
objectName: "mediaEmptyState"
visible: !root.hasMedia
width: parent.width
height: 56
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
font.family: "Symbols Nerd Font Mono"
font.pixelSize: 26
color: root.theme.ui.minor
text: "\uF001"
Accessible.ignored: true
}
Column {
anchors.left: parent.left
anchors.leftMargin: 40
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
NuguText {
theme: root.theme
variant: "title"
text: "nothing playing"
}
NuguText {
width: parent.width
theme: root.theme
variant: "meta"
text: "play media in an app to control it here"
color: root.theme.ui.minor
wrapMode: Text.WordWrap
}
}
}
}
}