feat: media player

This commit is contained in:
2026-09-21 21:50:32 +02:00
parent e0a27e59b4
commit 9b023a0993
5 changed files with 269 additions and 2 deletions
+10
View File
@@ -36,6 +36,16 @@ PluginComponent {
"y": 0.14, "y": 0.14,
"defaultVisible": true "defaultVisible": true
}, },
{
"id": "media",
"title": "Media",
"icon": "music_note",
"width": 430,
"height": 300,
"x": 0.04,
"y": 0.56,
"defaultVisible": true
},
{ {
"id": "home", "id": "home",
"title": "Home", "title": "Home",
+248
View File
@@ -0,0 +1,248 @@
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
}
}
}
+8
View File
@@ -104,6 +104,8 @@ FocusScope {
return audioWidgetComponent; return audioWidgetComponent;
case "apps": case "apps":
return appsWidgetComponent; return appsWidgetComponent;
case "media":
return mediaWidgetComponent;
case "home": case "home":
return homeWidgetComponent; return homeWidgetComponent;
case "system": case "system":
@@ -126,6 +128,12 @@ FocusScope {
GameBarAppMixer {} GameBarAppMixer {}
} }
Component {
id: mediaWidgetComponent
GameBarMediaWidget {}
}
Component { Component {
id: homeWidgetComponent id: homeWidgetComponent
+1 -1
View File
@@ -34,7 +34,7 @@ PluginSettings {
StyledText { StyledText {
width: parent.width width: parent.width
text: "Commands: dms ipc call gameBar toggle / open / close / widget <audio|apps|system> / resetLayout" text: "Commands: dms ipc call gameBar toggle / open / close / widget <audio|apps|media|home|system> / resetLayout"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: Theme.monoFontFamily font.family: Theme.monoFontFamily
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
+2 -1
View File
@@ -8,6 +8,7 @@ An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/
|---|---| |---|---|
| **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching | | **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching |
| **Apps** | Per-application volume sliders with individual mute, app icon and what is playing | | **Apps** | Per-application volume sliders with individual mute, app icon and what is playing |
| **Media** | Now playing with album art, seek bar, previous/play/next, and a chip per MPRIS source to switch between players |
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) | | **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
| **Home** | Home Assistant entities you pick: lights (toggle + brightness), numbers (slider over the entity's own min/max/step), switches/fans, media players (volume, mute, play/pause), covers, scenes and scripts, plus read-only sensors | | **Home** | Home Assistant entities you pick: lights (toggle + brightness), numbers (slider over the entity's own min/max/step), switches/fans, media players (volume, mute, play/pause), covers, scenes and scripts, plus read-only sensors |
@@ -39,7 +40,7 @@ Note that editing `plugin_settings.json` by hand does not reach a running shell.
| `dms ipc call gameBar toggle` | Toggle the overlay | | `dms ipc call gameBar toggle` | Toggle the overlay |
| `dms ipc call gameBar open` | Open the overlay | | `dms ipc call gameBar open` | Open the overlay |
| `dms ipc call gameBar close` | Close the overlay | | `dms ipc call gameBar close` | Close the overlay |
| `dms ipc call gameBar widget audio\|apps\|home\|system` | Show/hide one widget | | `dms ipc call gameBar widget audio\|apps\|media\|home\|system` | Show/hide one widget |
| `dms ipc call gameBar homeToggle <entity_id>` | Toggle an entity (activates a scene/script/button) | | `dms ipc call gameBar homeToggle <entity_id>` | Toggle an entity (activates a scene/script/button) |
| `dms ipc call gameBar homeRefresh` | Re-poll Home Assistant now | | `dms ipc call gameBar homeRefresh` | Re-poll Home Assistant now |
| `dms ipc call gameBar homeStatus` | Print URL, token length, HTTP status, entity/picked counts and last error | | `dms ipc call gameBar homeStatus` | Print URL, token length, HTTP status, entity/picked counts and last error |