diff --git a/GameBarDaemon.qml b/GameBarDaemon.qml index 35abf20..a004fbd 100644 --- a/GameBarDaemon.qml +++ b/GameBarDaemon.qml @@ -36,6 +36,16 @@ PluginComponent { "y": 0.14, "defaultVisible": true }, + { + "id": "media", + "title": "Media", + "icon": "music_note", + "width": 430, + "height": 300, + "x": 0.04, + "y": 0.56, + "defaultVisible": true + }, { "id": "home", "title": "Home", diff --git a/GameBarMediaWidget.qml b/GameBarMediaWidget.qml new file mode 100644 index 0000000..e1414ae --- /dev/null +++ b/GameBarMediaWidget.qml @@ -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 + } + } +} diff --git a/GameBarPanel.qml b/GameBarPanel.qml index ec52908..1433b53 100644 --- a/GameBarPanel.qml +++ b/GameBarPanel.qml @@ -104,6 +104,8 @@ FocusScope { return audioWidgetComponent; case "apps": return appsWidgetComponent; + case "media": + return mediaWidgetComponent; case "home": return homeWidgetComponent; case "system": @@ -126,6 +128,12 @@ FocusScope { GameBarAppMixer {} } + Component { + id: mediaWidgetComponent + + GameBarMediaWidget {} + } + Component { id: homeWidgetComponent diff --git a/GameBarSettings.qml b/GameBarSettings.qml index be7fde6..3e7d042 100644 --- a/GameBarSettings.qml +++ b/GameBarSettings.qml @@ -34,7 +34,7 @@ PluginSettings { StyledText { width: parent.width - text: "Commands: dms ipc call gameBar toggle / open / close / widget / resetLayout" + text: "Commands: dms ipc call gameBar toggle / open / close / widget / resetLayout" font.pixelSize: Theme.fontSizeSmall font.family: Theme.monoFontFamily color: Theme.surfaceVariantText diff --git a/README.md b/README.md index fe6c39e..516eca6 100644 --- a/README.md +++ b/README.md @@ -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 | | **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) | | **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 open` | Open 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 ` | Toggle an entity (activates a scene/script/button) | | `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 |