mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
Add media source switching
This commit is contained in:
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Cycle to the next media source and transfer playback when the current source is playing
|
||||
# omarchy:args=[next|previous]
|
||||
# omarchy:examples=omarchy audio source switch | omarchy-audio-source-switch previous
|
||||
|
||||
direction="${1:-next}"
|
||||
|
||||
case "$direction" in
|
||||
next)
|
||||
omarchy-shell media sourceSwitch
|
||||
;;
|
||||
previous)
|
||||
omarchy-shell media sourceSwitchPrevious
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-audio-source-switch [next|previous]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -24,6 +24,8 @@ o.bind("ALT + XF86MonBrightnessDown", "Brightness down precise", "omarchy-bright
|
||||
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("ALT + XF86AudioPause", "Switch media source", "omarchy-audio-source-switch", { locked = true })
|
||||
o.bind("ALT + XF86AudioPlay", "Switch media source", "omarchy-audio-source-switch", { 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("ALT + XF86AudioMute", "Switch audio output", "omarchy-audio-output-switch", { locked = true })
|
||||
|
||||
@@ -16,7 +16,10 @@ Item {
|
||||
property bool hasProgress: true
|
||||
property int duration: 1200
|
||||
readonly property int cardWidth: Style.space(269)
|
||||
readonly property int mediaCardWidth: Math.round(cardWidth * 1.5)
|
||||
readonly property int messageWidth: Style.space(190)
|
||||
readonly property int mediaMessageWidth: messageWidth + mediaCardWidth - cardWidth
|
||||
readonly property bool mediaOsd: iconKey.indexOf("media") === 0 || iconKey.indexOf("player") === 0
|
||||
|
||||
function iconFor(name, percent) {
|
||||
var n = String(name || "").toLowerCase()
|
||||
@@ -31,6 +34,7 @@ Item {
|
||||
if (n === "touchpad") return ""
|
||||
if (n === "touch" || n === "touchscreen") return ""
|
||||
if (n === "media" || n === "player") return ""
|
||||
if (n === "media-source" || n === "player-source") 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 ""
|
||||
@@ -95,7 +99,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: root.cardWidth
|
||||
width: root.mediaOsd ? root.mediaCardWidth : root.cardWidth
|
||||
height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -133,7 +137,7 @@ Item {
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: root.hasProgress ? Style.space(41) : root.messageWidth
|
||||
width: root.hasProgress ? Style.space(41) : (root.mediaOsd ? root.mediaMessageWidth : root.messageWidth)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.message
|
||||
font.family: Style.font.family
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -12,7 +13,17 @@ Item {
|
||||
property int playSerial: 0
|
||||
|
||||
readonly property var players: Mpris.players ? Mpris.players.values : []
|
||||
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
readonly property var playbackStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var n = nodes[i]
|
||||
if (n && n.isStream && isPlaybackStream(n) && n.audio) list.push(n)
|
||||
}
|
||||
return list
|
||||
}
|
||||
readonly property var sourcePlayers: orderedSourcePlayers()
|
||||
readonly property var sourceCyclePlayers: orderedCycleSourcePlayers()
|
||||
readonly property var activePlayer: selectActivePlayer()
|
||||
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||
readonly property string title: activePlayer ? (activePlayer.trackTitle || "") : ""
|
||||
@@ -35,6 +46,81 @@ Item {
|
||||
return !!(player && (player.trackTitle || player.trackArtist || player.trackAlbum || player.trackArtUrl))
|
||||
}
|
||||
|
||||
function playerCanControl(player) {
|
||||
return !!(player && (player.canTogglePlaying || player.canPlay || player.canPause || player.canGoNext || player.canGoPrevious))
|
||||
}
|
||||
|
||||
function canHandleAction(player, action) {
|
||||
if (!player) return false
|
||||
if (action === "next") return !!player.canGoNext
|
||||
if (action === "previous") return !!player.canGoPrevious
|
||||
if (action === "play") return !!(player.canPlay || player.canTogglePlaying)
|
||||
if (action === "pause") return !!(player.canPause || player.canTogglePlaying)
|
||||
if (action === "playPause") return !!(player.canTogglePlaying || player.canPlay || player.canPause)
|
||||
return false
|
||||
}
|
||||
|
||||
function canCycleSource(player) {
|
||||
return !!(player && hasMetadata(player) && (player.isPlaying || player.canPlay))
|
||||
}
|
||||
|
||||
function nodeProps(node) {
|
||||
return node && node.ready && node.properties ? node.properties : {}
|
||||
}
|
||||
|
||||
function isPlaybackStream(node) {
|
||||
if (!node || !node.isStream) return false
|
||||
if (node.isSink === true) return true
|
||||
|
||||
var mediaClass = String(node.type || "")
|
||||
return mediaClass.indexOf("Stream/Output/Audio") !== -1
|
||||
|| mediaClass.indexOf("AudioOutStream") !== -1
|
||||
|| mediaClass.indexOf("Output") !== -1
|
||||
}
|
||||
|
||||
function streamLabelKey(label) {
|
||||
var key = String(label || "").toLowerCase()
|
||||
key = key.replace(/^pipewire alsa \[/, "")
|
||||
key = key.replace(/\]$/, "")
|
||||
key = key.replace(/^alsa playback \[/, "")
|
||||
key = key.replace(/[^a-z0-9]+/g, "")
|
||||
return key
|
||||
}
|
||||
|
||||
function rawStreamLabel(node) {
|
||||
if (!node) return ""
|
||||
var p = nodeProps(node)
|
||||
return p["application.name"]
|
||||
|| node.description
|
||||
|| p["media.name"]
|
||||
|| p["node.name"]
|
||||
|| node.name
|
||||
}
|
||||
|
||||
function playerAppLabel(player) {
|
||||
if (!player) return ""
|
||||
var dbus = String(player.dbusName || "")
|
||||
dbus = dbus.replace(/^org\.mpris\.MediaPlayer2\./, "")
|
||||
dbus = dbus.replace(/\.instance[0-9]+$/, "")
|
||||
return player.desktopEntry || player.identity || dbus
|
||||
}
|
||||
|
||||
function playerHasPlaybackStream(player) {
|
||||
var playerKey = streamLabelKey(playerAppLabel(player))
|
||||
if (!playerKey) return false
|
||||
|
||||
for (var i = 0; i < playbackStreams.length; i++) {
|
||||
var streamKey = streamLabelKey(rawStreamLabel(playbackStreams[i]))
|
||||
if (!streamKey) continue
|
||||
if (streamKey === playerKey
|
||||
|| streamKey.indexOf(playerKey) !== -1
|
||||
|| playerKey.indexOf(streamKey) !== -1)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function playerKey(player) {
|
||||
if (!player) return ""
|
||||
return String(player.dbusName || player.desktopEntry || player.identity || "")
|
||||
@@ -102,7 +188,22 @@ Item {
|
||||
return list
|
||||
}
|
||||
|
||||
function oldestPlayingPlayer() {
|
||||
function orderedCycleSourcePlayers() {
|
||||
var list = []
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
var p = players[i]
|
||||
if (canCycleSource(p)) list.push(p)
|
||||
}
|
||||
|
||||
list.sort(function(a, b) {
|
||||
if (isProxyPlayer(a) !== isProxyPlayer(b)) return isProxyPlayer(a) ? 1 : -1
|
||||
return labelFor(a).localeCompare(labelFor(b))
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
function oldestPlayingPlayer(requirePlaybackStream) {
|
||||
var oldest = null
|
||||
var oldestOrder = 0
|
||||
var playingProxy = null
|
||||
@@ -114,6 +215,8 @@ Item {
|
||||
|
||||
var proxyPlayer = isProxyPlayer(p)
|
||||
if (p.isPlaying) {
|
||||
if (requirePlaybackStream && !playerHasPlaybackStream(p)) continue
|
||||
|
||||
var order = playerOrder(p, i + 1000)
|
||||
if (!proxyPlayer && (!oldest || order < oldestOrder)) {
|
||||
oldest = p
|
||||
@@ -132,6 +235,10 @@ Item {
|
||||
var preferred = null
|
||||
var trackPlayer = null
|
||||
var trackProxy = null
|
||||
var streamPlayer = null
|
||||
var streamProxy = null
|
||||
var controllablePlayer = null
|
||||
var controllableProxy = null
|
||||
var identityPlayer = null
|
||||
var identityProxy = null
|
||||
|
||||
@@ -143,9 +250,15 @@ Item {
|
||||
|
||||
if (preferredPlayerKey && playerKey(p) === preferredPlayerKey && hasMetadata(p)) preferred = p
|
||||
|
||||
if (hasTrackMetadata(p)) {
|
||||
if (playerHasPlaybackStream(p)) {
|
||||
if (!proxy && !streamPlayer) streamPlayer = p
|
||||
else if (proxy && !streamProxy) streamProxy = p
|
||||
} else if (hasTrackMetadata(p)) {
|
||||
if (!proxy && !trackPlayer) trackPlayer = p
|
||||
else if (proxy && !trackProxy) trackProxy = p
|
||||
} else if (playerCanControl(p)) {
|
||||
if (!proxy && !controllablePlayer) controllablePlayer = p
|
||||
else if (proxy && !controllableProxy) controllableProxy = p
|
||||
} else if (hasMetadata(p)) {
|
||||
if (!proxy && !identityPlayer) identityPlayer = p
|
||||
else if (proxy && !identityProxy) identityProxy = p
|
||||
@@ -153,7 +266,9 @@ Item {
|
||||
}
|
||||
|
||||
if (preferred && preferred.isPlaying) return preferred
|
||||
return oldestPlayingPlayer() || preferred || trackPlayer || trackProxy || identityPlayer || identityProxy || null
|
||||
var streamCandidate = streamPlayer || streamProxy
|
||||
var streamPreferred = preferred && playerHasPlaybackStream(preferred) ? preferred : null
|
||||
return oldestPlayingPlayer(true) || oldestPlayingPlayer(false) || streamPreferred || streamCandidate || preferred || trackPlayer || trackProxy || controllablePlayer || controllableProxy || identityPlayer || identityProxy || null
|
||||
}
|
||||
|
||||
function labelFor(player) {
|
||||
@@ -183,8 +298,30 @@ Item {
|
||||
return true
|
||||
}
|
||||
|
||||
function switchSource(delta) {
|
||||
var list = sourcePlayers
|
||||
function playPlayer(player) {
|
||||
if (!player) return false
|
||||
if (player.canPlay) {
|
||||
player.play()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function pausePlayer(player) {
|
||||
if (!player) return false
|
||||
if (player.canPause) {
|
||||
player.pause()
|
||||
return true
|
||||
}
|
||||
if (player.canTogglePlaying && player.isPlaying) {
|
||||
player.togglePlaying()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function switchSource(delta, transferPlayback, showFeedback) {
|
||||
var list = sourceCyclePlayers
|
||||
if (!list || list.length === 0) return false
|
||||
|
||||
var activeKey = playerKey(activePlayer)
|
||||
@@ -197,7 +334,24 @@ Item {
|
||||
}
|
||||
|
||||
index = (index + delta + list.length) % list.length
|
||||
preferredPlayerKey = playerKey(list[index])
|
||||
var current = activePlayer
|
||||
var next = list[index]
|
||||
var currentWasPlaying = current && current.isPlaying
|
||||
var currentKey = playerKey(current)
|
||||
var nextKey = playerKey(next)
|
||||
|
||||
preferredPlayerKey = nextKey
|
||||
|
||||
if (transferPlayback && currentWasPlaying && next && nextKey !== currentKey) {
|
||||
var nextWasPlaying = next.isPlaying
|
||||
var nextStarted = nextWasPlaying || playPlayer(next)
|
||||
if (nextStarted) pausePlayer(current)
|
||||
}
|
||||
|
||||
if (showFeedback !== false) Qt.callLater(function() {
|
||||
root.showOsd("Source", "media-source", next)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -206,10 +360,17 @@ Item {
|
||||
if (targeted) return targeted
|
||||
|
||||
if (action === "pause" || action === "playPause") {
|
||||
var oldest = oldestPlayingPlayer()
|
||||
var oldest = oldestPlayingPlayer(true) || oldestPlayingPlayer(false)
|
||||
if (oldest) return oldest
|
||||
}
|
||||
|
||||
if (canHandleAction(activePlayer, action)) return activePlayer
|
||||
|
||||
var list = sourcePlayers
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (canHandleAction(list[i], action)) return list[i]
|
||||
}
|
||||
|
||||
return activePlayer
|
||||
}
|
||||
|
||||
@@ -282,6 +443,8 @@ Item {
|
||||
onTriggered: root.syncPlayingOrder()
|
||||
}
|
||||
|
||||
PwObjectTracker { objects: root.playbackStreams }
|
||||
|
||||
function statusJson() {
|
||||
var p = activePlayer
|
||||
return JSON.stringify({
|
||||
@@ -328,11 +491,19 @@ Item {
|
||||
}
|
||||
|
||||
function sourceNext(): string {
|
||||
return root.switchSource(1) ? "ok" : "unhandled"
|
||||
return root.switchSource(1, false, true) ? "ok" : "unhandled"
|
||||
}
|
||||
|
||||
function sourcePrevious(): string {
|
||||
return root.switchSource(-1) ? "ok" : "unhandled"
|
||||
return root.switchSource(-1, false, true) ? "ok" : "unhandled"
|
||||
}
|
||||
|
||||
function sourceSwitch(): string {
|
||||
return root.switchSource(1, true, true) ? "ok" : "unhandled"
|
||||
}
|
||||
|
||||
function sourceSwitchPrevious(): string {
|
||||
return root.switchSource(-1, true, true) ? "ok" : "unhandled"
|
||||
}
|
||||
|
||||
function ping(): string {
|
||||
|
||||
Reference in New Issue
Block a user