mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
101b27fcc7
Large commit that totally refactor of the way we handle the bar and panels. Testing should focus on Panels, Bar, Keyboard Focus, IPC calls. Changes brief: - One NFullScreenWindow per screen which handle it's bar and dedicated panels. - Added shadows - Reintroduced dimming - New panels animations - Proper Z ordering - Panels on overlay laywer is not reimplemented, if we do it then the bar will be on the Overlay too - Panel dragging was not reimplemented, to be discussed before reimplementing - Still a WIP, need to work more on shadows and polishing + debugging.
326 lines
11 KiB
QML
326 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()
|
|
}
|
|
}
|
|
}
|