mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
Detailed Explanation: Addresses an issue where the media player would automatically switch,overriding user's manual selections when the Control Center was open. The core problem was that the automatic player detection logic was too aggressive and didn't respect explicit user choices. Previous attempts to pause auto-switching based on UI visibility were unreliable due to incorrect event handling for the custom NPanel component.
325 lines
11 KiB
QML
325 lines
11 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Services.Mpris
|
|
import qs.Commons
|
|
import qs.Services
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property var currentPlayer: null
|
|
property real currentPosition: 0
|
|
property bool isSeeking: false
|
|
property int selectedPlayerIndex: 0
|
|
property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing || currentPlayer.isPlaying) : false
|
|
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle !== undefined ? currentPlayer.trackTitle.replace(/(\r\n|\n|\r)/g, "") : "") : ""
|
|
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : ""
|
|
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : ""
|
|
property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
|
|
property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0
|
|
property bool canPlay: currentPlayer ? currentPlayer.canPlay : false
|
|
property bool canPause: currentPlayer ? currentPlayer.canPause : false
|
|
property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false
|
|
property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false
|
|
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
|
|
property real infiniteTrackLength: 922337203685
|
|
|
|
Component.onCompleted: {
|
|
updateCurrentPlayer()
|
|
}
|
|
|
|
function getAvailablePlayers() {
|
|
if (!Mpris.players || !Mpris.players.values) {
|
|
return []
|
|
}
|
|
|
|
let allPlayers = Mpris.players.values
|
|
let finalPlayers = []
|
|
const genericBrowsers = ["firefox", "chromium", "chrome"]
|
|
const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []
|
|
|
|
// Separate players into specific and generic lists
|
|
let specificPlayers = []
|
|
let genericPlayers = []
|
|
for (var i = 0; i < allPlayers.length; i++) {
|
|
const identity = String(allPlayers[i].identity || "").toLowerCase()
|
|
const match = blacklist.find(b => {
|
|
const s = String(b || "").toLowerCase()
|
|
return s && (identity.includes(s))
|
|
})
|
|
if (match)
|
|
continue
|
|
if (genericBrowsers.some(b => identity.includes(b))) {
|
|
genericPlayers.push(allPlayers[i])
|
|
} else {
|
|
specificPlayers.push(allPlayers[i])
|
|
}
|
|
}
|
|
|
|
let matchedGenericIndices = {}
|
|
|
|
// For each specific player, try to find and pair it with a generic partner
|
|
for (var i = 0; i < specificPlayers.length; i++) {
|
|
let specificPlayer = specificPlayers[i]
|
|
let title1 = String(specificPlayer.trackTitle || "").trim()
|
|
let wasMatched = false
|
|
|
|
if (title1) {
|
|
for (var j = 0; j < genericPlayers.length; j++) {
|
|
if (matchedGenericIndices[j])
|
|
continue
|
|
let genericPlayer = genericPlayers[j]
|
|
let title2 = String(genericPlayer.trackTitle || "").trim()
|
|
|
|
if (title2 && (title1.includes(title2) || title2.includes(title1))) {
|
|
let dataPlayer = genericPlayer
|
|
let identityPlayer = specificPlayer
|
|
|
|
let scoreSpecific = (specificPlayer.trackArtUrl ? 1 : 0)
|
|
let scoreGeneric = (genericPlayer.trackArtUrl ? 1 : 0)
|
|
if (scoreSpecific > scoreGeneric) {
|
|
dataPlayer = specificPlayer
|
|
}
|
|
|
|
let virtualPlayer = {
|
|
"identity": identityPlayer.identity,
|
|
"desktopEntry": identityPlayer.desktopEntry,
|
|
"trackTitle": dataPlayer.trackTitle,
|
|
"trackArtist": dataPlayer.trackArtist,
|
|
"trackAlbum": dataPlayer.trackAlbum,
|
|
"trackArtUrl": dataPlayer.trackArtUrl,
|
|
"length": dataPlayer.length || 0,
|
|
"position": dataPlayer.position || 0,
|
|
"playbackState": dataPlayer.playbackState,
|
|
"isPlaying": dataPlayer.isPlaying || false,
|
|
"canPlay": dataPlayer.canPlay || false,
|
|
"canPause": dataPlayer.canPause || false,
|
|
"canGoNext": dataPlayer.canGoNext || false,
|
|
"canGoPrevious": dataPlayer.canGoPrevious || false,
|
|
"canSeek": dataPlayer.canSeek || false,
|
|
"canControl": dataPlayer.canControl || false,
|
|
"_stateSource": dataPlayer,
|
|
"_controlTarget": identityPlayer
|
|
}
|
|
finalPlayers.push(virtualPlayer)
|
|
matchedGenericIndices[j] = true
|
|
wasMatched = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (!wasMatched) {
|
|
finalPlayers.push(specificPlayer)
|
|
}
|
|
}
|
|
|
|
// Add any generic players that were not matched
|
|
for (var i = 0; i < genericPlayers.length; i++) {
|
|
if (!matchedGenericIndices[i]) {
|
|
finalPlayers.push(genericPlayers[i])
|
|
}
|
|
}
|
|
|
|
// Filter for controllable players
|
|
let controllablePlayers = []
|
|
for (var i = 0; i < finalPlayers.length; i++) {
|
|
let player = finalPlayers[i]
|
|
if (player && player.canControl) {
|
|
controllablePlayers.push(player)
|
|
}
|
|
}
|
|
return controllablePlayers
|
|
}
|
|
|
|
function findActivePlayer() {
|
|
let availablePlayers = getAvailablePlayers()
|
|
if (availablePlayers.length === 0) {
|
|
//Logger.i("Media", "No active player found")
|
|
return null
|
|
}
|
|
|
|
// Prioritize the actively playing player ---
|
|
for (var i = 0; i < availablePlayers.length; i++) {
|
|
if (availablePlayers[i] && availablePlayers[i].playbackState === MprisPlaybackState.Playing) {
|
|
Logger.d("Media", "Found actively playing player: " + availablePlayers[i].identity)
|
|
selectedPlayerIndex = i
|
|
return availablePlayers[i]
|
|
}
|
|
}
|
|
|
|
// fallback if nothing is playing)
|
|
const preferred = (Settings.data.audio.preferredPlayer || "")
|
|
if (preferred !== "") {
|
|
for (var i = 0; i < availablePlayers.length; i++) {
|
|
const p = availablePlayers[i]
|
|
const identity = String(p.identity || "").toLowerCase()
|
|
const pref = preferred.toLowerCase()
|
|
if (identity.includes(pref)) {
|
|
selectedPlayerIndex = i
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedPlayerIndex < availablePlayers.length) {
|
|
return availablePlayers[selectedPlayerIndex]
|
|
} else {
|
|
selectedPlayerIndex = 0
|
|
return availablePlayers[0]
|
|
}
|
|
}
|
|
|
|
property bool autoSwitchingPaused: false
|
|
|
|
function switchToPlayer(index) {
|
|
let availablePlayers = getAvailablePlayers()
|
|
if (index >= 0 && index < availablePlayers.length) {
|
|
let newPlayer = availablePlayers[index]
|
|
if (newPlayer !== currentPlayer) {
|
|
currentPlayer = newPlayer
|
|
selectedPlayerIndex = index
|
|
currentPosition = currentPlayer ? currentPlayer.position : 0
|
|
Logger.d("Media", "Manually switched to player " + currentPlayer.identity)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Switch to the most recently active player
|
|
function updateCurrentPlayer() {
|
|
let newPlayer = findActivePlayer()
|
|
if (newPlayer !== currentPlayer) {
|
|
currentPlayer = newPlayer
|
|
currentPosition = currentPlayer ? currentPlayer.position : 0
|
|
Logger.d("Media", "Switching player")
|
|
}
|
|
}
|
|
|
|
function playPause() {
|
|
if (currentPlayer) {
|
|
let stateSource = currentPlayer._stateSource || currentPlayer
|
|
let controlTarget = currentPlayer._controlTarget || currentPlayer
|
|
if (stateSource.playbackState === MprisPlaybackState.Playing) {
|
|
controlTarget.pause()
|
|
} else {
|
|
controlTarget.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
function play() {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canPlay) {
|
|
target.play()
|
|
}
|
|
}
|
|
|
|
function stop() {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target) {
|
|
target.stop()
|
|
}
|
|
}
|
|
|
|
function pause() {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canPause) {
|
|
target.pause()
|
|
}
|
|
}
|
|
|
|
function next() {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canGoNext) {
|
|
target.next()
|
|
}
|
|
}
|
|
|
|
function previous() {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canGoPrevious) {
|
|
target.previous()
|
|
}
|
|
}
|
|
|
|
function seek(position) {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canSeek) {
|
|
target.position = position
|
|
currentPosition = position
|
|
}
|
|
}
|
|
|
|
// Seek to position based on ratio (0.0 to 1.0)
|
|
function seekByRatio(ratio) {
|
|
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
|
|
if (target && target.canSeek && target.length > 0) {
|
|
let seekPosition = ratio * target.length
|
|
target.position = seekPosition
|
|
currentPosition = seekPosition
|
|
}
|
|
}
|
|
|
|
// Update progress bar every second while playing
|
|
Timer {
|
|
id: positionTimer
|
|
interval: 1000
|
|
running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0 && currentPlayer.playbackState === MprisPlaybackState.Playing
|
|
repeat: true
|
|
onTriggered: {
|
|
if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) {
|
|
currentPosition = currentPlayer.position
|
|
} else {
|
|
running = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid overwriting currentPosition while seeking due to backend position changes
|
|
Connections {
|
|
target: currentPlayer
|
|
function onPositionChanged() {
|
|
if (!root.isSeeking && currentPlayer) {
|
|
currentPosition = currentPlayer.position
|
|
}
|
|
}
|
|
function onPlaybackStateChanged() {
|
|
if (!root.isSeeking && currentPlayer) {
|
|
currentPosition = currentPlayer.position
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset position when switching to inactive player
|
|
onCurrentPlayerChanged: {
|
|
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
|
|
currentPosition = 0
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: playerStateMonitor
|
|
interval: 2000 // Check every 2 seconds
|
|
repeat: true
|
|
running: true
|
|
onTriggered: {
|
|
Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused)
|
|
if (autoSwitchingPaused) return
|
|
// Only update if we don't have a playing player or if current player is paused
|
|
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
|
|
updateCurrentPlayer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update current player when available players change
|
|
Connections {
|
|
target: Mpris.players
|
|
function onValuesChanged() {
|
|
Logger.d("Media", "Players changed")
|
|
updateCurrentPlayer()
|
|
}
|
|
}
|
|
}
|