364 lines
12 KiB
QML
364 lines
12 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell.Services.Mpris
|
|
import "../../../config" as Cfg
|
|
|
|
Item {
|
|
id: root
|
|
|
|
implicitHeight: contentCol.implicitHeight
|
|
|
|
readonly property var t: Cfg.Config.theme
|
|
|
|
property var activePlayer: null
|
|
|
|
readonly property bool canChangeVolume: root.activePlayer && root.activePlayer.volumeSupported && root.activePlayer.canControl
|
|
|
|
readonly property var _identityMap: ({
|
|
"firefox": "Firefox",
|
|
"spotify": "Spotify",
|
|
"chromium": "Chromium",
|
|
"google-chrome": "Chrome",
|
|
"brave": "Brave",
|
|
"vlc": "VLC",
|
|
"mpv": "mpv"
|
|
})
|
|
|
|
function selectPlayer() {
|
|
var players = Mpris.players.values
|
|
var fallback = null
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
if (!p || (p.trackTitle === "" && p.identity === "")) continue
|
|
if (p.playbackState === MprisPlaybackState.Playing) { activePlayer = p; return }
|
|
if (!fallback) fallback = p
|
|
}
|
|
if (activePlayer && activePlayer.playbackState !== MprisPlaybackState.Stopped) return
|
|
activePlayer = fallback
|
|
}
|
|
|
|
Instantiator {
|
|
model: Mpris.players
|
|
Connections {
|
|
required property MprisPlayer modelData
|
|
target: modelData
|
|
|
|
Component.onCompleted: {
|
|
if (root.activePlayer == null || modelData.playbackState === MprisPlaybackState.Playing)
|
|
root.selectPlayer()
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (root.activePlayer === modelData) {
|
|
root.activePlayer = null
|
|
root.selectPlayer()
|
|
}
|
|
}
|
|
|
|
function onPlaybackStateChanged() {
|
|
root.selectPlayer()
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatDuration(s) {
|
|
if (isNaN(s) || s < 0) return "0:00"
|
|
var m = Math.floor(s / 60)
|
|
var sec = Math.floor(s % 60)
|
|
return m + ":" + (sec < 10 ? "0" : "") + sec
|
|
}
|
|
|
|
function formatIdentity(name) {
|
|
if (!name) return "Media"
|
|
var lower = name.toLowerCase()
|
|
for (var key in root._identityMap) {
|
|
if (lower.indexOf(key) !== -1) return root._identityMap[key]
|
|
}
|
|
return name
|
|
}
|
|
|
|
function playbackStatusText() {
|
|
if (!root.activePlayer) return "No players"
|
|
switch (root.activePlayer.playbackState) {
|
|
case MprisPlaybackState.Playing: return "Now playing"
|
|
case MprisPlaybackState.Paused: return "Paused"
|
|
default: return "Stopped"
|
|
}
|
|
}
|
|
|
|
function volumeIcon() {
|
|
if (!root.activePlayer) return ""
|
|
var v = root.activePlayer.volume
|
|
if (v === 0) return ""
|
|
if (v < 0.33) return ""
|
|
if (v < 0.66) return ""
|
|
return ""
|
|
}
|
|
|
|
Timer {
|
|
id: positionTimer
|
|
running: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing
|
|
interval: 1000
|
|
repeat: true
|
|
onTriggered: {
|
|
if (root.activePlayer) root.activePlayer.positionChanged()
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: contentCol
|
|
anchors { left: parent.left; right: parent.right; top: parent.top }
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 32
|
|
color: "transparent"
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 10
|
|
|
|
Rectangle {
|
|
width: 32; height: 32
|
|
radius: 8
|
|
color: Qt.alpha(root.t.mprisAccent, 0.12)
|
|
border.color: Qt.alpha(root.t.mprisAccent, 0.22)
|
|
border.width: 1
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: ""
|
|
color: root.t.mprisAccent
|
|
font.pixelSize: 18
|
|
font.weight: Font.Bold
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
Text {
|
|
text: root.activePlayer ? root.formatIdentity(root.activePlayer.identity) : "No media"
|
|
color: root.t.mprisTextMain
|
|
font.pixelSize: 13
|
|
font.weight: Font.Bold
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
Text {
|
|
text: root.playbackStatusText()
|
|
color: root.t.mprisTextDim
|
|
font.pixelSize: 10
|
|
font.weight: Font.Bold
|
|
opacity: 0.75
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 12 }
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop { position: 0.0; color: Qt.alpha(root.t.mprisAccent, 0.5) }
|
|
GradientStop { position: 1.0; color: "transparent" }
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: root.activePlayer ? 10 : 0 }
|
|
|
|
ColumnLayout {
|
|
id: trackArea
|
|
Layout.fillWidth: true
|
|
visible: root.activePlayer !== null
|
|
spacing: 0
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
Rectangle {
|
|
width: 64; height: 64
|
|
radius: 8
|
|
color: root.t.mprisAlbumBg
|
|
border.color: root.t.mprisAlbumBorder
|
|
border.width: 1
|
|
Layout.alignment: Qt.AlignTop
|
|
|
|
Image {
|
|
id: albumArt
|
|
anchors.fill: parent
|
|
anchors.margins: 2
|
|
source: root.activePlayer ? root.activePlayer.trackArtUrl : ""
|
|
fillMode: Image.PreserveAspectCrop
|
|
visible: root.activePlayer && root.activePlayer.trackArtUrl !== "" && status === Image.Ready
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: ""
|
|
color: root.t.mprisAccent
|
|
font.pixelSize: 28
|
|
font.weight: Font.Bold
|
|
visible: root.activePlayer && (root.activePlayer.trackArtUrl === "" || !albumArt.visible)
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
spacing: 2
|
|
|
|
Text {
|
|
text: root.activePlayer ? (root.activePlayer.trackTitle || "Unknown Title") : ""
|
|
color: root.t.mprisTextMain
|
|
font.pixelSize: 13
|
|
font.weight: Font.Bold
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: root.activePlayer ? (root.activePlayer.trackArtist || "Unknown Artist") : ""
|
|
color: root.t.mprisTextDim
|
|
font.pixelSize: 11
|
|
font.weight: Font.Bold
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: root.activePlayer ? (root.activePlayer.trackAlbum || "") : ""
|
|
color: root.t.mprisTextDim
|
|
font.pixelSize: 10
|
|
font.weight: Font.Bold
|
|
opacity: 0.7
|
|
elide: Text.ElideRight
|
|
visible: root.activePlayer && root.activePlayer.trackAlbum !== ""
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 12 }
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
visible: root.activePlayer && root.activePlayer.positionSupported
|
|
|
|
MediaSlider {
|
|
Layout.fillWidth: true
|
|
interactive: root.activePlayer && root.activePlayer.canSeek
|
|
fraction: {
|
|
var p = root.activePlayer
|
|
if (!p) return 0
|
|
var len = p.length
|
|
if (len > 0) return Math.max(0, Math.min(1, p.position / len))
|
|
return 0
|
|
}
|
|
onSeeked: frac => {
|
|
var p = root.activePlayer
|
|
if (p && p.canSeek && p.length > 0) {
|
|
p.position = frac * p.length
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Text {
|
|
text: root.activePlayer ? root.formatDuration(root.activePlayer.position) : "0:00"
|
|
color: root.t.mprisTextDim
|
|
font.pixelSize: 9
|
|
font.weight: Font.Bold
|
|
Layout.alignment: Qt.AlignLeft
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
Text {
|
|
text: root.activePlayer ? root.formatDuration(root.activePlayer.length) : "0:00"
|
|
color: root.t.mprisTextDim
|
|
font.pixelSize: 9
|
|
font.weight: Font.Bold
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 12 }
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
visible: root.canChangeVolume
|
|
|
|
Text {
|
|
text: root.volumeIcon()
|
|
color: root.activePlayer && root.activePlayer.volume > 0 ? root.t.mprisAccent : root.t.mprisTextDim
|
|
font.pixelSize: 16
|
|
font.weight: Font.Bold
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
MediaSlider {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
interactive: root.canChangeVolume
|
|
fraction: {
|
|
var p = root.activePlayer
|
|
if (!p) return 1
|
|
return p.volume
|
|
}
|
|
onSeeked: frac => {
|
|
var p = root.activePlayer
|
|
if (p && root.canChangeVolume) {
|
|
p.volume = frac
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: root.canChangeVolume ? 12 : 0 }
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: 16
|
|
|
|
MediaButton {
|
|
icon: ""
|
|
enabled: root.activePlayer && root.activePlayer.canGoPrevious
|
|
onClicked: root.activePlayer.previous()
|
|
}
|
|
|
|
MediaButton {
|
|
primary: true
|
|
icon: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing ? "" : ""
|
|
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
|
|
onClicked: root.activePlayer.togglePlaying()
|
|
}
|
|
|
|
MediaButton {
|
|
icon: ""
|
|
enabled: root.activePlayer && root.activePlayer.canGoNext
|
|
onClicked: root.activePlayer.next()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 6 }
|
|
}
|
|
}
|