Switch media controls to QS MPRIS
This commit is contained in:
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Control media playback and show the Omarchy OSD
|
|
||||||
# omarchy:args=<next|previous|play-pause|play|pause>
|
|
||||||
# omarchy:examples=omarchy audio player next | omarchy audio player play-pause
|
|
||||||
|
|
||||||
action="${1:-play-pause}"
|
|
||||||
|
|
||||||
case "$action" in
|
|
||||||
next) fallback_message="Next track" ;;
|
|
||||||
previous) fallback_message="Previous track" ;;
|
|
||||||
play) fallback_message="Playing" ;;
|
|
||||||
pause) fallback_message="Paused" ;;
|
|
||||||
*) fallback_message="Play/pause" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
playerctl "$action" >/dev/null 2>&1 || true
|
|
||||||
sleep 0.05
|
|
||||||
|
|
||||||
message=$(playerctl metadata --format '{{title}}' 2>/dev/null || true)
|
|
||||||
[[ -n $message ]] || message=$(playerctl metadata --format '{{playerName}}' 2>/dev/null || true)
|
|
||||||
|
|
||||||
omarchy-osd -i media -m "${message:-$fallback_message}"
|
|
||||||
@@ -21,9 +21,9 @@ o.bind("ALT + XF86MonBrightnessUp", "Brightness up precise", "omarchy-brightness
|
|||||||
o.bind("ALT + XF86MonBrightnessDown", "Brightness down precise", "omarchy-brightness-display 1%-", { locked = true, repeating = true })
|
o.bind("ALT + XF86MonBrightnessDown", "Brightness down precise", "omarchy-brightness-display 1%-", { locked = true, repeating = true })
|
||||||
|
|
||||||
-- Media controls.
|
-- Media controls.
|
||||||
o.bind("XF86AudioNext", "Next track", "omarchy-audio-player next", { locked = true })
|
o.bind("XF86AudioNext", "Next track", "omarchy-shell media next", { locked = true })
|
||||||
o.bind("XF86AudioPause", "Pause", "omarchy-audio-player play-pause", { locked = true })
|
o.bind("XF86AudioPause", "Pause", "omarchy-shell media playPause", { locked = true })
|
||||||
o.bind("XF86AudioPlay", "Play", "omarchy-audio-player play-pause", { locked = true })
|
o.bind("XF86AudioPlay", "Play", "omarchy-shell media playPause", { locked = true })
|
||||||
o.bind("XF86AudioPrev", "Previous track", "omarchy-audio-player previous", { locked = true })
|
o.bind("XF86AudioPrev", "Previous track", "omarchy-shell media previous", { locked = true })
|
||||||
|
|
||||||
o.bind("SUPER + XF86AudioMute", "Switch audio output", "omarchy-audio-output-switch", { locked = true })
|
o.bind("SUPER + XF86AudioMute", "Switch audio output", "omarchy-audio-output-switch", { locked = true })
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ obsidian
|
|||||||
omarchy-nvim
|
omarchy-nvim
|
||||||
pamixer
|
pamixer
|
||||||
pinta
|
pinta
|
||||||
playerctl
|
|
||||||
plocate
|
plocate
|
||||||
plymouth
|
plymouth
|
||||||
postgresql-libs
|
postgresql-libs
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
echo "Remove playerctl now that media keys use Quickshell MPRIS"
|
||||||
|
|
||||||
|
omarchy-pkg-drop playerctl
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import qs.Ui
|
import qs.Ui
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
|
||||||
@@ -8,18 +7,8 @@ BarWidget {
|
|||||||
id: root
|
id: root
|
||||||
moduleName: "Media"
|
moduleName: "Media"
|
||||||
|
|
||||||
|
readonly property var mediaService: bar && bar.shell ? bar.shell.firstPartyServiceFor("omarchy.media") : null
|
||||||
readonly property var players: Mpris.players ? Mpris.players.values : []
|
readonly property var activePlayer: mediaService ? mediaService.activePlayer : null
|
||||||
readonly property var activePlayer: {
|
|
||||||
var playing = null
|
|
||||||
for (var i = 0; i < players.length; i++) {
|
|
||||||
var p = players[i]
|
|
||||||
if (!p) continue
|
|
||||||
if (p.isPlaying) return p
|
|
||||||
if (!playing && p.trackTitle) playing = p
|
|
||||||
}
|
|
||||||
return playing
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
|
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||||
readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "" : ""
|
readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "" : ""
|
||||||
@@ -90,17 +79,17 @@ BarWidget {
|
|||||||
onClicked: function(mouse) {
|
onClicked: function(mouse) {
|
||||||
if (!root.activePlayer) return
|
if (!root.activePlayer) return
|
||||||
if (mouse.button === Qt.MiddleButton) {
|
if (mouse.button === Qt.MiddleButton) {
|
||||||
if (root.activePlayer.canGoNext) root.activePlayer.next()
|
if (root.mediaService) root.mediaService.runAction("next", false)
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
root.popupOpen = !root.popupOpen
|
root.popupOpen = !root.popupOpen
|
||||||
} else {
|
} else {
|
||||||
if (root.activePlayer.canTogglePlaying) root.activePlayer.togglePlaying()
|
if (root.mediaService) root.mediaService.runAction("playPause", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onWheel: function(wheel) {
|
onWheel: function(wheel) {
|
||||||
if (!root.activePlayer) return
|
if (!root.activePlayer) return
|
||||||
if (wheel.angleDelta.y > 0 && root.activePlayer.canGoPrevious) root.activePlayer.previous()
|
if (wheel.angleDelta.y > 0 && root.mediaService) root.mediaService.runAction("previous", false)
|
||||||
else if (wheel.angleDelta.y < 0 && root.activePlayer.canGoNext) root.activePlayer.next()
|
else if (wheel.angleDelta.y < 0 && root.mediaService) root.mediaService.runAction("next", false)
|
||||||
}
|
}
|
||||||
onEntered: if (root.bar) root.bar.showTooltip(root, root.hasMedia ? (root.title + (root.artist ? " — " + root.artist : "")) : "")
|
onEntered: if (root.bar) root.bar.showTooltip(root, root.hasMedia ? (root.title + (root.artist ? " — " + root.artist : "")) : "")
|
||||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||||
@@ -198,7 +187,7 @@ BarWidget {
|
|||||||
verticalPadding: Style.spacing.controlPaddingY
|
verticalPadding: Style.spacing.controlPaddingY
|
||||||
enabled: root.activePlayer && root.activePlayer.canGoPrevious
|
enabled: root.activePlayer && root.activePlayer.canGoPrevious
|
||||||
opacity: enabled ? 1.0 : 0.4
|
opacity: enabled ? 1.0 : 0.4
|
||||||
onClicked: if (root.activePlayer) root.activePlayer.previous()
|
onClicked: if (root.mediaService) root.mediaService.runAction("previous", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -209,7 +198,7 @@ BarWidget {
|
|||||||
iconSize: Style.font.iconLarge
|
iconSize: Style.font.iconLarge
|
||||||
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
|
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
|
||||||
opacity: enabled ? 1.0 : 0.4
|
opacity: enabled ? 1.0 : 0.4
|
||||||
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
|
onClicked: if (root.mediaService) root.mediaService.runAction("playPause", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -219,7 +208,7 @@ BarWidget {
|
|||||||
verticalPadding: Style.spacing.controlPaddingY
|
verticalPadding: Style.spacing.controlPaddingY
|
||||||
enabled: root.activePlayer && root.activePlayer.canGoNext
|
enabled: root.activePlayer && root.activePlayer.canGoNext
|
||||||
opacity: enabled ? 1.0 : 0.4
|
opacity: enabled ? 1.0 : 0.4
|
||||||
onClicked: if (root.activePlayer) root.activePlayer.next()
|
onClicked: if (root.mediaService) root.mediaService.runAction("next", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ Item {
|
|||||||
property bool opened: false
|
property bool opened: false
|
||||||
property string icon: ""
|
property string icon: ""
|
||||||
property string message: ""
|
property string message: ""
|
||||||
|
property string iconKey: ""
|
||||||
property int value: 0
|
property int value: 0
|
||||||
property int maxValue: 100
|
property int maxValue: 100
|
||||||
property bool hasProgress: true
|
property bool hasProgress: true
|
||||||
property int duration: 1200
|
property int duration: 1200
|
||||||
|
readonly property int cardWidth: Style.space(269)
|
||||||
|
readonly property int messageWidth: Style.space(190)
|
||||||
|
|
||||||
function iconFor(name, percent) {
|
function iconFor(name, percent) {
|
||||||
var n = String(name || "").toLowerCase()
|
var n = String(name || "").toLowerCase()
|
||||||
@@ -28,6 +31,10 @@ Item {
|
|||||||
if (n === "touchpad") return ""
|
if (n === "touchpad") return ""
|
||||||
if (n === "touch" || n === "touchscreen") return ""
|
if (n === "touch" || n === "touchscreen") return ""
|
||||||
if (n === "media" || n === "player") return ""
|
if (n === "media" || n === "player") return ""
|
||||||
|
if (n === "media-play" || n === "player-play") return ""
|
||||||
|
if (n === "media-pause" || n === "player-pause") return ""
|
||||||
|
if (n === "media-next" || n === "player-next") return ""
|
||||||
|
if (n === "media-previous" || n === "player-previous") return ""
|
||||||
if (n.length > 0) return name
|
if (n.length > 0) return name
|
||||||
if (percent <= 0) return ""
|
if (percent <= 0) return ""
|
||||||
if (percent <= 33) return ""
|
if (percent <= 33) return ""
|
||||||
@@ -36,6 +43,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
|
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
|
||||||
|
iconKey = String(iconName || "").toLowerCase()
|
||||||
maxValue = Math.max(1, parseInt(rawMax || "100", 10))
|
maxValue = Math.max(1, parseInt(rawMax || "100", 10))
|
||||||
var parsed = parseInt(rawValue || "0", 10)
|
var parsed = parseInt(rawValue || "0", 10)
|
||||||
hasProgress = rawValue !== "" && !isNaN(parsed) && rawMessage === ""
|
hasProgress = rawValue !== "" && !isNaN(parsed) && rawMessage === ""
|
||||||
@@ -87,7 +95,7 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: card
|
id: card
|
||||||
width: Style.space(269)
|
width: root.cardWidth
|
||||||
height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap)
|
height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
@@ -125,7 +133,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
width: root.hasProgress ? Style.space(41) : Style.space(190)
|
width: root.hasProgress ? Style.space(41) : root.messageWidth
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.message
|
text: root.message
|
||||||
font.family: Style.font.family
|
font.family: Style.font.family
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var shell: null
|
||||||
|
property string preferredPlayerKey: ""
|
||||||
|
|
||||||
|
readonly property var players: Mpris.players ? Mpris.players.values : []
|
||||||
|
readonly property var activePlayer: selectActivePlayer()
|
||||||
|
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||||
|
readonly property string title: activePlayer ? (activePlayer.trackTitle || "") : ""
|
||||||
|
readonly property string artist: activePlayer ? (activePlayer.trackArtist || "") : ""
|
||||||
|
readonly property string album: activePlayer && activePlayer.trackAlbum ? activePlayer.trackAlbum : ""
|
||||||
|
readonly property string artUrl: activePlayer && activePlayer.trackArtUrl ? activePlayer.trackArtUrl : ""
|
||||||
|
readonly property string identity: activePlayer ? (activePlayer.identity || activePlayer.desktopEntry || "") : ""
|
||||||
|
|
||||||
|
function isProxyPlayer(player) {
|
||||||
|
var dbusName = String(player && player.dbusName || "").toLowerCase()
|
||||||
|
var desktopEntry = String(player && player.desktopEntry || "").toLowerCase()
|
||||||
|
return dbusName.indexOf("playerctld") !== -1 || desktopEntry === "playerctld"
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMetadata(player) {
|
||||||
|
return !!(player && (player.trackTitle || player.trackArtist || player.identity || player.desktopEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTrackMetadata(player) {
|
||||||
|
return !!(player && (player.trackTitle || player.trackArtist || player.trackAlbum || player.trackArtUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerKey(player) {
|
||||||
|
if (!player) return ""
|
||||||
|
return String(player.dbusName || player.desktopEntry || player.identity || "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectActivePlayer() {
|
||||||
|
var playingProxy = null
|
||||||
|
var preferred = null
|
||||||
|
var trackPlayer = null
|
||||||
|
var trackProxy = null
|
||||||
|
var identityPlayer = null
|
||||||
|
var identityProxy = null
|
||||||
|
|
||||||
|
for (var i = 0; i < players.length; i++) {
|
||||||
|
var p = players[i]
|
||||||
|
if (!p) continue
|
||||||
|
|
||||||
|
var proxy = isProxyPlayer(p)
|
||||||
|
if (p.isPlaying) {
|
||||||
|
if (!proxy) return p
|
||||||
|
if (!playingProxy) playingProxy = p
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredPlayerKey && playerKey(p) === preferredPlayerKey && hasMetadata(p)) preferred = p
|
||||||
|
|
||||||
|
if (hasTrackMetadata(p)) {
|
||||||
|
if (!proxy && !trackPlayer) trackPlayer = p
|
||||||
|
else if (proxy && !trackProxy) trackProxy = p
|
||||||
|
} else if (hasMetadata(p)) {
|
||||||
|
if (!proxy && !identityPlayer) identityPlayer = p
|
||||||
|
else if (proxy && !identityProxy) identityProxy = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playingProxy || preferred || trackPlayer || trackProxy || identityPlayer || identityProxy || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelFor(player) {
|
||||||
|
if (!player) return ""
|
||||||
|
return player.trackTitle || player.identity || player.desktopEntry || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function osdMessage(player, fallback) {
|
||||||
|
if (!player) return fallback
|
||||||
|
var label = labelFor(player)
|
||||||
|
if (label && player.trackArtist) return label + " - " + player.trackArtist
|
||||||
|
return label || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOsd(actionLabel, iconName) {
|
||||||
|
if (!shell) return
|
||||||
|
shell.summon("omarchy.osd", JSON.stringify({
|
||||||
|
icon: iconName || "media",
|
||||||
|
message: osdMessage(activePlayer, actionLabel)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAction(action, showFeedback) {
|
||||||
|
var player = activePlayer
|
||||||
|
var key = playerKey(player)
|
||||||
|
var actionLabel = "Play/pause"
|
||||||
|
var iconName = "media"
|
||||||
|
var handled = false
|
||||||
|
|
||||||
|
if (action === "next") {
|
||||||
|
actionLabel = "Next"
|
||||||
|
iconName = "media-next"
|
||||||
|
if (player && player.canGoNext) {
|
||||||
|
player.next()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
} else if (action === "previous") {
|
||||||
|
actionLabel = "Previous"
|
||||||
|
iconName = "media-previous"
|
||||||
|
if (player && player.canGoPrevious) {
|
||||||
|
player.previous()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
} else if (action === "play") {
|
||||||
|
actionLabel = "Play"
|
||||||
|
iconName = "media-play"
|
||||||
|
if (player && player.canPlay) {
|
||||||
|
player.play()
|
||||||
|
handled = true
|
||||||
|
} else if (player && player.canTogglePlaying && !player.isPlaying) {
|
||||||
|
player.togglePlaying()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
} else if (action === "pause") {
|
||||||
|
actionLabel = "Pause"
|
||||||
|
iconName = "media-pause"
|
||||||
|
if (player && player.canPause) {
|
||||||
|
player.pause()
|
||||||
|
handled = true
|
||||||
|
} else if (player && player.canTogglePlaying && player.isPlaying) {
|
||||||
|
player.togglePlaying()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
} else if (action === "playPause") {
|
||||||
|
actionLabel = player && player.isPlaying ? "Pause" : "Play"
|
||||||
|
iconName = player && player.isPlaying ? "media-pause" : "media-play"
|
||||||
|
if (player && player.isPlaying && player.canPause) {
|
||||||
|
player.pause()
|
||||||
|
handled = true
|
||||||
|
} else if (player && !player.isPlaying && player.canPlay) {
|
||||||
|
player.play()
|
||||||
|
handled = true
|
||||||
|
} else if (player && player.canTogglePlaying) {
|
||||||
|
player.togglePlaying()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled && key) preferredPlayerKey = key
|
||||||
|
if (showFeedback !== false) Qt.callLater(function() { root.showOsd(actionLabel, iconName) })
|
||||||
|
return handled
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusJson() {
|
||||||
|
var p = activePlayer
|
||||||
|
return JSON.stringify({
|
||||||
|
hasPlayer: p !== null,
|
||||||
|
hasMedia: root.hasMedia,
|
||||||
|
playing: p ? !!p.isPlaying : false,
|
||||||
|
identity: p ? (p.identity || "") : "",
|
||||||
|
desktopEntry: p ? (p.desktopEntry || "") : "",
|
||||||
|
title: p ? (p.trackTitle || "") : "",
|
||||||
|
artist: p ? (p.trackArtist || "") : "",
|
||||||
|
album: p && p.trackAlbum ? p.trackAlbum : "",
|
||||||
|
artUrl: p && p.trackArtUrl ? p.trackArtUrl : "",
|
||||||
|
canGoNext: p ? !!p.canGoNext : false,
|
||||||
|
canGoPrevious: p ? !!p.canGoPrevious : false,
|
||||||
|
canTogglePlaying: p ? !!p.canTogglePlaying : false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "media"
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return root.statusJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause(): string {
|
||||||
|
return root.runAction("playPause", true) ? "ok" : "unhandled"
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(): string {
|
||||||
|
return root.runAction("next", true) ? "ok" : "unhandled"
|
||||||
|
}
|
||||||
|
|
||||||
|
function previous(): string {
|
||||||
|
return root.runAction("previous", true) ? "ok" : "unhandled"
|
||||||
|
}
|
||||||
|
|
||||||
|
function play(): string {
|
||||||
|
return root.runAction("play", true) ? "ok" : "unhandled"
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause(): string {
|
||||||
|
return root.runAction("pause", true) ? "ok" : "unhandled"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ping(): string {
|
||||||
|
return "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "omarchy.media",
|
||||||
|
"name": "Media",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "MPRIS media control service",
|
||||||
|
"kinds": [
|
||||||
|
"service"
|
||||||
|
],
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": {
|
||||||
|
"service": "Service.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user