diff --git a/bin/omarchy-audio-player b/bin/omarchy-audio-player deleted file mode 100755 index 7219634c..00000000 --- a/bin/omarchy-audio-player +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Control media playback and show the Omarchy OSD -# omarchy:args= -# 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}" diff --git a/default/hypr/bindings/media.lua b/default/hypr/bindings/media.lua index 05e3fcad..0d443f6d 100644 --- a/default/hypr/bindings/media.lua +++ b/default/hypr/bindings/media.lua @@ -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 }) -- Media controls. -o.bind("XF86AudioNext", "Next track", "omarchy-audio-player next", { locked = true }) -o.bind("XF86AudioPause", "Pause", "omarchy-audio-player play-pause", { locked = true }) -o.bind("XF86AudioPlay", "Play", "omarchy-audio-player play-pause", { locked = true }) -o.bind("XF86AudioPrev", "Previous track", "omarchy-audio-player previous", { locked = true }) +o.bind("XF86AudioNext", "Next track", "omarchy-shell media next", { locked = true }) +o.bind("XF86AudioPause", "Pause", "omarchy-shell media playPause", { locked = true }) +o.bind("XF86AudioPlay", "Play", "omarchy-shell media playPause", { 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 }) diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 05696f53..ca207a09 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -94,7 +94,6 @@ obsidian omarchy-nvim pamixer pinta -playerctl plocate plymouth postgresql-libs diff --git a/migrations/1779309700.sh b/migrations/1779309700.sh new file mode 100644 index 00000000..f4ab0c62 --- /dev/null +++ b/migrations/1779309700.sh @@ -0,0 +1,3 @@ +echo "Remove playerctl now that media keys use Quickshell MPRIS" + +omarchy-pkg-drop playerctl diff --git a/shell/plugins/bar/widgets/Media.qml b/shell/plugins/bar/widgets/Media.qml index ae3317c2..78776d03 100644 --- a/shell/plugins/bar/widgets/Media.qml +++ b/shell/plugins/bar/widgets/Media.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Services.Mpris import qs.Ui import qs.Commons @@ -8,18 +7,8 @@ BarWidget { id: root moduleName: "Media" - - readonly property var players: Mpris.players ? Mpris.players.values : [] - 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 var mediaService: bar && bar.shell ? bar.shell.firstPartyServiceFor("omarchy.media") : null + readonly property var activePlayer: mediaService ? mediaService.activePlayer : null readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist) readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "󰏤" : "󰐊" @@ -90,17 +79,17 @@ BarWidget { onClicked: function(mouse) { if (!root.activePlayer) return 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) { root.popupOpen = !root.popupOpen } else { - if (root.activePlayer.canTogglePlaying) root.activePlayer.togglePlaying() + if (root.mediaService) root.mediaService.runAction("playPause", false) } } onWheel: function(wheel) { if (!root.activePlayer) return - if (wheel.angleDelta.y > 0 && root.activePlayer.canGoPrevious) root.activePlayer.previous() - else if (wheel.angleDelta.y < 0 && root.activePlayer.canGoNext) root.activePlayer.next() + if (wheel.angleDelta.y > 0 && root.mediaService) root.mediaService.runAction("previous", false) + 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 : "")) : "") onExited: if (root.bar) root.bar.hideTooltip(root) @@ -198,7 +187,7 @@ BarWidget { verticalPadding: Style.spacing.controlPaddingY enabled: root.activePlayer && root.activePlayer.canGoPrevious opacity: enabled ? 1.0 : 0.4 - onClicked: if (root.activePlayer) root.activePlayer.previous() + onClicked: if (root.mediaService) root.mediaService.runAction("previous", false) } Button { @@ -209,7 +198,7 @@ BarWidget { iconSize: Style.font.iconLarge enabled: root.activePlayer && root.activePlayer.canTogglePlaying opacity: enabled ? 1.0 : 0.4 - onClicked: if (root.activePlayer) root.activePlayer.togglePlaying() + onClicked: if (root.mediaService) root.mediaService.runAction("playPause", false) } Button { @@ -219,7 +208,7 @@ BarWidget { verticalPadding: Style.spacing.controlPaddingY enabled: root.activePlayer && root.activePlayer.canGoNext opacity: enabled ? 1.0 : 0.4 - onClicked: if (root.activePlayer) root.activePlayer.next() + onClicked: if (root.mediaService) root.mediaService.runAction("next", false) } } } diff --git a/shell/plugins/osd/Osd.qml b/shell/plugins/osd/Osd.qml index ad06180d..6e853ac0 100644 --- a/shell/plugins/osd/Osd.qml +++ b/shell/plugins/osd/Osd.qml @@ -10,10 +10,13 @@ Item { property bool opened: false property string icon: "" property string message: "" + property string iconKey: "" property int value: 0 property int maxValue: 100 property bool hasProgress: true property int duration: 1200 + readonly property int cardWidth: Style.space(269) + readonly property int messageWidth: Style.space(190) function iconFor(name, percent) { var n = String(name || "").toLowerCase() @@ -28,6 +31,10 @@ Item { if (n === "touchpad") return "󰟸" if (n === "touch" || n === "touchscreen") 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 (percent <= 0) return "" if (percent <= 33) return "" @@ -36,6 +43,7 @@ Item { } function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) { + iconKey = String(iconName || "").toLowerCase() maxValue = Math.max(1, parseInt(rawMax || "100", 10)) var parsed = parseInt(rawValue || "0", 10) hasProgress = rawValue !== "" && !isNaN(parsed) && rawMessage === "" @@ -87,7 +95,7 @@ Item { Rectangle { id: card - width: Style.space(269) + width: root.cardWidth height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap) anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -125,7 +133,7 @@ Item { } } Text { - width: root.hasProgress ? Style.space(41) : Style.space(190) + width: root.hasProgress ? Style.space(41) : root.messageWidth anchors.verticalCenter: parent.verticalCenter text: root.message font.family: Style.font.family diff --git a/shell/plugins/services/media/Service.qml b/shell/plugins/services/media/Service.qml new file mode 100644 index 00000000..765e81bb --- /dev/null +++ b/shell/plugins/services/media/Service.qml @@ -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" + } + } +} diff --git a/shell/plugins/services/media/manifest.json b/shell/plugins/services/media/manifest.json new file mode 100644 index 00000000..bdf932ac --- /dev/null +++ b/shell/plugins/services/media/manifest.json @@ -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" + } +}