import qs.modules.common import qs.modules.common.widgets import qs.services import qs import qs.modules.common.functions import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import Quickshell.Wayland import Quickshell.Hyprland Scope { id: root property bool visible: false readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property var realPlayers: Mpris.players.values.filter(player => isRealPlayer(player)) readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers) readonly property real osdWidth: Appearance.sizes.osdWidth readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight property real contentPadding: 13 property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1 property real artRounding: Appearance.rounding.verysmall property list visualizerPoints: [] property bool hasPlasmaIntegration: true Process { id: plasmaIntegrationAvailabilityCheckProc running: true command: ["bash", "-c", "command -v plasma-browser-integration-host"] onExited: (exitCode, exitStatus) => { root.hasPlasmaIntegration = (exitCode === 0); } } function isRealPlayer(player) { // return true return ( // Remove unecessary native buses from browsers if there's plasma integration !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) && // playerctld just copies other buses and we don't need duplicates !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && // Non-instance mpd bus !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')) ); } function filterDuplicatePlayers(players) { let filtered = []; let used = new Set(); for (let i = 0; i < players.length; ++i) { if (used.has(i)) continue; let p1 = players[i]; let group = [i]; // Find duplicates by trackTitle prefix for (let j = i + 1; j < players.length; ++j) { let p2 = players[j]; if (p1.trackTitle && p2.trackTitle && (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle)) || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) { group.push(j); } } // Pick the one with non-empty trackArtUrl, or fallback to the first let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0); if (chosenIdx === undefined) chosenIdx = group[0]; filtered.push(players[chosenIdx]); group.forEach(idx => used.add(idx)); } return filtered; } Process { id: cavaProc running: mediaControlsLoader.active onRunningChanged: { if (!cavaProc.running) { root.visualizerPoints = []; } } command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`] stdout: SplitParser { onRead: data => { // Parse `;`-separated values into the visualizerPoints array let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p)); root.visualizerPoints = points; } } } Loader { id: mediaControlsLoader active: GlobalStates.mediaControlsOpen onActiveChanged: { if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) { GlobalStates.mediaControlsOpen = false; } } sourceComponent: PanelWindow { id: mediaControlsRoot visible: true exclusiveZone: 0 implicitWidth: ( (mediaControlsRoot.screen.width / 2) // Middle of screen - (osdWidth / 2) // Dodge OSD - (widgetWidth / 2) // Account for widget width ) * 2 implicitHeight: playerColumnLayout.implicitHeight color: "transparent" WlrLayershell.namespace: "quickshell:mediaControls" anchors { top: !Config.options.bar.bottom bottom: Config.options.bar.bottom left: true } mask: Region { item: playerColumnLayout } ColumnLayout { id: playerColumnLayout anchors.top: parent.top anchors.bottom: parent.bottom x: (mediaControlsRoot.screen.width / 2) // Middle of screen - (osdWidth / 2) // Dodge OSD - (widgetWidth) // Account for widget width + (Appearance.sizes.elevationMargin) // It's fine for shadows to overlap spacing: -Appearance.sizes.elevationMargin // Shadow overlap okay Repeater { model: ScriptModel { values: root.meaningfulPlayers } delegate: PlayerControl { required property MprisPlayer modelData player: modelData visualizerPoints: root.visualizerPoints } } } } } IpcHandler { target: "mediaControls" function toggle(): void { mediaControlsLoader.active = !mediaControlsLoader.active; if(mediaControlsLoader.active) Notifications.timeoutAll(); } function close(): void { mediaControlsLoader.active = false; } function open(): void { mediaControlsLoader.active = true; Notifications.timeoutAll(); } } GlobalShortcut { name: "mediaControlsToggle" description: "Toggles media controls on press" onPressed: { GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen; } } GlobalShortcut { name: "mediaControlsOpen" description: "Opens media controls on press" onPressed: { GlobalStates.mediaControlsOpen = true; } } GlobalShortcut { name: "mediaControlsClose" description: "Closes media controls on press" onPressed: { GlobalStates.mediaControlsOpen = false; } } }