mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-03 12:47:50 +02:00
469 lines
13 KiB
QML
469 lines
13 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Mpris
|
|
import Quickshell.Services.Pipewire
|
|
import "MediaModel.js" as MediaModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var shell: null
|
|
property string preferredPlayerKey: ""
|
|
property var playerStartedAt: ({})
|
|
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 || "") : ""
|
|
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) {
|
|
return MediaModel.isProxyPlayer(player)
|
|
}
|
|
|
|
function hasMetadata(player) {
|
|
return MediaModel.hasMetadata(player)
|
|
}
|
|
|
|
function hasTrackMetadata(player) {
|
|
return MediaModel.hasTrackMetadata(player)
|
|
}
|
|
|
|
function playerCanControl(player) {
|
|
return MediaModel.playerCanControl(player)
|
|
}
|
|
|
|
function canHandleAction(player, action) {
|
|
return MediaModel.canHandleAction(player, action)
|
|
}
|
|
|
|
function canCycleSource(player) {
|
|
return MediaModel.canCycleSource(player)
|
|
}
|
|
|
|
function nodeProps(node) {
|
|
return MediaModel.nodeProps(node)
|
|
}
|
|
|
|
function isPlaybackStream(node) {
|
|
return MediaModel.isPlaybackStream(node)
|
|
}
|
|
|
|
function streamLabelKey(label) {
|
|
return MediaModel.streamLabelKey(label)
|
|
}
|
|
|
|
function rawStreamLabel(node) {
|
|
return MediaModel.rawStreamLabel(node)
|
|
}
|
|
|
|
function playerAppLabel(player) {
|
|
return MediaModel.playerAppLabel(player)
|
|
}
|
|
|
|
function playerHasPlaybackStream(player) {
|
|
return MediaModel.playerHasPlaybackStream(player, playbackStreams)
|
|
}
|
|
|
|
function playerKey(player) {
|
|
return MediaModel.playerKey(player)
|
|
}
|
|
|
|
function playerForKey(key) {
|
|
if (!key) return null
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
if (playerKey(p) === key) return p
|
|
}
|
|
return null
|
|
}
|
|
|
|
function playerOrder(player, fallback) {
|
|
var key = playerKey(player)
|
|
var value = key ? playerStartedAt[key] : undefined
|
|
return value === undefined ? fallback : value
|
|
}
|
|
|
|
function syncPlayingOrder() {
|
|
var next = {}
|
|
var alive = {}
|
|
var serial = playSerial
|
|
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
var key = playerKey(p)
|
|
if (!key) continue
|
|
|
|
alive[key] = true
|
|
if (!p.isPlaying) continue
|
|
|
|
if (playerStartedAt[key] === undefined) {
|
|
serial += 1
|
|
next[key] = serial
|
|
} else {
|
|
next[key] = playerStartedAt[key]
|
|
}
|
|
}
|
|
|
|
if (preferredPlayerKey && !alive[preferredPlayerKey]) preferredPlayerKey = ""
|
|
|
|
playSerial = serial
|
|
playerStartedAt = next
|
|
}
|
|
|
|
function orderedSourcePlayers() {
|
|
var list = []
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
if (hasMetadata(p)) list.push(p)
|
|
}
|
|
|
|
list.sort(function(a, b) {
|
|
if (!!a.isPlaying !== !!b.isPlaying) return a.isPlaying ? -1 : 1
|
|
if (isProxyPlayer(a) !== isProxyPlayer(b)) return isProxyPlayer(a) ? 1 : -1
|
|
if (a.isPlaying && b.isPlaying) {
|
|
var orderDelta = playerOrder(a, 1000) - playerOrder(b, 1000)
|
|
if (orderDelta !== 0) return orderDelta
|
|
}
|
|
return labelFor(a).localeCompare(labelFor(b))
|
|
})
|
|
|
|
return list
|
|
}
|
|
|
|
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
|
|
var proxyOrder = 0
|
|
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
if (!p) continue
|
|
|
|
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
|
|
oldestOrder = order
|
|
} else if (proxyPlayer && (!playingProxy || order < proxyOrder)) {
|
|
playingProxy = p
|
|
proxyOrder = order
|
|
}
|
|
}
|
|
}
|
|
|
|
return oldest || playingProxy || null
|
|
}
|
|
|
|
function selectActivePlayer() {
|
|
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
|
|
|
|
for (var i = 0; i < players.length; i++) {
|
|
var p = players[i]
|
|
if (!p) continue
|
|
|
|
var proxy = isProxyPlayer(p)
|
|
|
|
if (preferredPlayerKey && playerKey(p) === preferredPlayerKey && hasMetadata(p)) preferred = 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
|
|
}
|
|
}
|
|
|
|
if (preferred && preferred.isPlaying) return preferred
|
|
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) {
|
|
return MediaModel.labelFor(player)
|
|
}
|
|
|
|
function osdMessage(player, fallback) {
|
|
return MediaModel.osdMessage(player, fallback)
|
|
}
|
|
|
|
function showOsd(actionLabel, iconName, player) {
|
|
if (!shell) return
|
|
shell.summon("omarchy.osd", JSON.stringify({
|
|
icon: iconName || "media",
|
|
message: osdMessage(player || activePlayer, actionLabel)
|
|
}))
|
|
}
|
|
|
|
function selectPlayer(key) {
|
|
var player = playerForKey(key)
|
|
if (!player || !hasMetadata(player)) return false
|
|
preferredPlayerKey = playerKey(player)
|
|
return true
|
|
}
|
|
|
|
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)
|
|
var index = 0
|
|
for (var i = 0; i < list.length; i++) {
|
|
if (playerKey(list[i]) === activeKey) {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
|
|
index = (index + delta + list.length) % list.length
|
|
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
|
|
}
|
|
|
|
function playerForAction(action, targetKey) {
|
|
var targeted = playerForKey(targetKey)
|
|
if (targeted) return targeted
|
|
|
|
if (action === "pause" || action === "playPause") {
|
|
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
|
|
}
|
|
|
|
function runAction(action, showFeedback, targetKey) {
|
|
var player = playerForAction(action, targetKey)
|
|
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, player) })
|
|
return handled
|
|
}
|
|
|
|
Timer {
|
|
interval: 500
|
|
running: true
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.syncPlayingOrder()
|
|
}
|
|
|
|
PwObjectTracker { objects: root.playbackStreams }
|
|
|
|
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 sourceNext(): string {
|
|
return root.switchSource(1, false, true) ? "ok" : "unhandled"
|
|
}
|
|
|
|
function sourcePrevious(): string {
|
|
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 {
|
|
return "ok"
|
|
}
|
|
}
|
|
}
|