Files
quickdots/widgets/sidebar/mpris/MprisWidget.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 }
}
}