add noctalia and fuzzel
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds) || seconds < 0)
|
||||
return "0:00";
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
var s = Math.floor(seconds % 60);
|
||||
var pad = function (n) {
|
||||
return (n < 10) ? ("0" + n) : n;
|
||||
};
|
||||
|
||||
if (h > 0) {
|
||||
return h + ":" + pad(m) + ":" + pad(s);
|
||||
} else {
|
||||
return m + ":" + pad(s);
|
||||
}
|
||||
}
|
||||
|
||||
property var currentPlayer: null
|
||||
property string playerIdentity: currentPlayer ? (currentPlayer.identity || "") : ""
|
||||
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 string positionString: formatTime(currentPosition)
|
||||
property string lengthString: formatTime(trackLength)
|
||||
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++) {
|
||||
if (!allPlayers[i])
|
||||
continue;
|
||||
const identity = String(allPlayers[i].identity || "").toLowerCase();
|
||||
const dbusName = String(allPlayers[i].dbusName || "").toLowerCase();
|
||||
const match = blacklist.find(b => {
|
||||
const s = String(b || "").toLowerCase();
|
||||
return s && (identity.includes(s) || dbusName.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.canPlay) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function seekRelative(offset) {
|
||||
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null;
|
||||
if (target && target.canSeek && target.length > 0) {
|
||||
let seekPosition = target.position + offset;
|
||||
target.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// TODO Remove in may 2026
|
||||
Component.onCompleted: {
|
||||
_setBandsCount();
|
||||
}
|
||||
|
||||
// Register a component that needs audio data, call this when a visualizer becomes active.
|
||||
// Pass a unique identifier (e.g., "lockscreen", "controlcenter:screen1", "plugin:fancy-audiovisualizer")
|
||||
function registerComponent(componentId) {
|
||||
root._registeredComponents[componentId] = true;
|
||||
root._registeredComponents = Object.assign({}, root._registeredComponents);
|
||||
Logger.d("Spectrum", "Component registered:", componentId, "- total:", root._registeredCount);
|
||||
}
|
||||
|
||||
// Unregister a component when it no longer needs audio data.
|
||||
function unregisterComponent(componentId) {
|
||||
delete root._registeredComponents[componentId];
|
||||
root._registeredComponents = Object.assign({}, root._registeredComponents);
|
||||
Logger.d("Spectrum", "Component unregistered:", componentId, "- total:", root._registeredCount);
|
||||
}
|
||||
|
||||
// Check if a component is registered
|
||||
function isRegistered(componentId) {
|
||||
return root._registeredComponents[componentId] === true;
|
||||
}
|
||||
|
||||
// Component registration - any component needing audio data registers here
|
||||
property var _registeredComponents: ({})
|
||||
readonly property int _registeredCount: Object.keys(_registeredComponents).length
|
||||
property bool _shouldRun: _registeredCount > 0
|
||||
|
||||
property var values: []
|
||||
property bool isIdle: true
|
||||
|
||||
PwAudioSpectrum {
|
||||
id: spectrum
|
||||
node: Pipewire.defaultAudioSink
|
||||
enabled: root._shouldRun
|
||||
// TODO Uncomment this in may 2026
|
||||
// bandCount: Settings.data.audio.spectrumMirrored ? 32 : 64
|
||||
frameRate: Settings.data.audio.spectrumFrameRate
|
||||
lowerCutoff: 50
|
||||
upperCutoff: 12000
|
||||
noiseReduction: 0.77
|
||||
smoothing: true
|
||||
|
||||
onValuesChanged: {
|
||||
root.values = spectrum.values;
|
||||
}
|
||||
|
||||
onIdleChanged: {
|
||||
root.isIdle = spectrum.idle;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Remove in may 2026 - temporary until noctalia-qs is fully propagated
|
||||
Connections {
|
||||
target: Settings.data.audio
|
||||
function onSpectrumMirroredChanged() {
|
||||
_setBandsCount();
|
||||
}
|
||||
}
|
||||
function _setBandsCount() {
|
||||
const bandCount = Settings.data.audio.spectrumMirrored ? 32 : 64;
|
||||
if (spectrum.bandCount !== undefined) {
|
||||
spectrum.bandCount = bandCount;
|
||||
} else if (spectrum.barCount !== undefined) {
|
||||
spectrum.barCount = bandCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user