repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/Media.qml
Rawimport QtQuick
import Quickshell.Services.Mpris
Item {
id: root
property Osd osdService
property Audio audioService
property string manualSelection: ""
property var lastAction: ({})
property var trackChange: ({
"direction": "none",
"reverse": false,
"fromTitle": "",
"toTitle": ""
})
readonly property bool nativeActionRoute: true
// cheap reactive "something playing" signal for the bar chip active dot
readonly property bool playing: {
const players = Mpris.players.values || [];
for (let i = 0; i < players.length; i++)
if (players[i].playbackState === MprisPlaybackState.Playing)
return true;
return false;
}
function playbackStateText(player) {
return String(MprisPlaybackState.toString(player.playbackState || MprisPlaybackState.Stopped)).toLowerCase();
}
function loopStateText(player) {
const text = String(MprisLoopState.toString(player.loopState || MprisLoopState.None)).toLowerCase();
if (text === "playlist")
return "all";
if (text === "track")
return "one";
return "none";
}
function playerId(player) {
return String(player.dbusName || player.uniqueId || player.identity || "mpris-player");
}
function nativePlayerById(id) {
const players = Mpris.players.values || [];
for (let i = 0; i < players.length; i++)
if (playerId(players[i]) === id) {
return players[i];
}
return null;
}
function clonePlayer(player) {
const id = playerId(player);
const title = String(player.trackTitle || "");
const artist = String(player.trackArtist || "");
const album = String(player.trackAlbum || "");
const position = Math.round(Number(player.position || 0));
const length = Math.max(0, Math.round(Number(player.length || 0)));
const cover = String(player.trackArtUrl || "");
return {
"id": id,
"identity": String(player.identity || id),
"title": title,
"artist": artist,
"album": album,
"playbackState": playbackStateText(player),
"position": position,
"length": length,
"progress": length > 0 ? Math.round(position * 1000 / length) / 1000 : 0,
"shuffle": !!player.shuffle,
"repeat": loopStateText(player),
"canSeek": !!player.canSeek,
"canShuffle": !!player.shuffleSupported,
"canRepeat": !!player.loopSupported,
"cover": cover,
"artAvailable": cover.length > 0
};
}
function servicePlayers() {
return Mpris.players.values || [];
}
function activeIndex() {
const players = servicePlayers();
if (manualSelection.length > 0) {
for (let i = 0; i < players.length; i++)
if (clonePlayer(players[i]).id === manualSelection) {
return i;
}
}
for (let j = 0; j < players.length; j++)
if (clonePlayer(players[j]).playbackState === "playing") {
return j;
}
return players.length > 0 ? 0 : -1;
}
function activePlayerRaw() {
const players = servicePlayers();
const index = activeIndex();
return index >= 0 ? players[index] : ({
"dbusName": "",
"identity": "",
"trackTitle": "",
"trackArtist": "",
"trackAlbum": "",
"playbackState": MprisPlaybackState.Stopped,
"position": 0,
"length": 1,
"shuffle": false,
"loopState": MprisLoopState.None,
"trackArtUrl": ""
});
}
function activePlayer() {
return clonePlayer(activePlayerRaw());
}
function osd(eventText) {
if (!osdService)
return {
"event": "media",
"value": eventText,
"role": "info"
};
try {
return JSON.parse(osdService.trigger("media", eventText));
} catch (error) {
return {
"event": "media",
"value": eventText,
"role": "info"
};
}
}
function setTrackChange(direction, reverse, fromTitle, toTitle) {
trackChange = {
"direction": direction,
"reverse": reverse,
"fromTitle": fromTitle,
"toTitle": toTitle
};
}
function mediaProgress() {
return Math.max(0, Math.min(1, Number(activePlayer().progress || 0)));
}
function mediaPlayingValue() {
return activePlayer().playbackState === "playing" ? 1 : 0;
}
function mediaEnergyValue() {
// Amplitude VU removed (no audio capture). Energy now reflects playback state only.
return mediaPlayingValue();
}
function mediaModeValue() {
const active = activePlayer();
if (active.shuffle)
return 1;
return active.repeat === "none" ? 0 : 0.5;
}
function mediaPanelContract() {
return {
"source": "Nuguland dashboard media Details.qml plus Nuguland MprisController.qml",
"ownedBy": "PopoutHost.qml inline MediaPanel",
"coverDetailsProgressTransport": true,
"playerSelectorRail": true,
"trackDirectionAnimation": ["previous", "next"],
"seekPreviewBinding": "positionLabel follows slider while dragging",
"transportActions": ["shuffle", "previous", "playPause", "next", "repeat"],
"noExternalVisualizer": true,
"serviceActionsOnly": true,
"coverShape": "nugu-rounded-card",
"progressRail": "mpris-progress"
};
}
function stateObject(osdState) {
const active = activePlayer();
return {
"backend": "Media.qml",
"usesNativeMpris": true,
"nativeActionRoute": nativeActionRoute,
"externalVisualizer": false,
"manualSelection": manualSelection,
"players": servicePlayers().map(player => {
return clonePlayer(player);
}),
"activePlayer": active,
"cover": {
"source": active.artAvailable ? active.cover : `nugu-media:${active.id}`,
"artAvailable": active.artAvailable,
"role": active.artAvailable ? "content.normal" : "ui.accent"
},
"compact": {
"label": "Media",
"title": active.title,
"artist": active.artist,
"playing": active.playbackState === "playing"
},
"shader": {
"kind": "media-waveform",
"clipped": true,
"source": "shaders/media-waveform.frag.qsb",
"priorArt": "WebAudio/ISF visualizer research: true music visualizers need waveform/FFT textures; current chip uses native PipeWire peak history as a compact amplitude visualizer",
"values": [mediaProgress(), mediaPlayingValue(), mediaEnergyValue(), mediaModeValue()],
"colors": ["ui.accent", "ui.focus", "ui.minor", "ui.backdrop", "warning", "error"]
},
"rich": {
"hasTransport": true,
"hasProgress": true,
"hasPlayerSelection": true,
"canSeek": active.canSeek,
"shuffle": active.shuffle,
"repeat": active.repeat
},
"audioHandoff": {
"route": "Audio.qml",
"reason": "media volume stays owned by Audio.qml",
"openTarget": "audio",
"serviceBoundary": true
},
"mediaPanelContract": mediaPanelContract(),
"lineMotif": {
"source": "mpris-progress",
"role": "ui.accent",
"progress": active.progress
},
"trackChange": trackChange,
"lastAction": lastAction,
"osd": osdState || {}
};
}
function stateJson() {
return JSON.stringify(stateObject({}));
}
function action(kind) {
const active = activePlayer();
const playerIdValue = active.id;
const name = String(kind);
const nativePlayer = nativePlayerById(playerIdValue);
if (name === "next")
setTrackChange("next", false, active.title, "pending");
else if (name === "previous")
setTrackChange("previous", true, active.title, "pending");
if (nativePlayer) {
if (name === "playPause")
nativePlayer.togglePlaying();
else if (name === "next" && nativePlayer.canGoNext)
nativePlayer.next();
else if (name === "previous" && nativePlayer.canGoPrevious)
nativePlayer.previous();
else if (name === "stop")
nativePlayer.stop();
else if (name === "shuffle" && nativePlayer.shuffleSupported)
nativePlayer.shuffle = !nativePlayer.shuffle;
else if (name === "repeat" && nativePlayer.loopSupported)
nativePlayer.loopState = nativePlayer.loopState === MprisLoopState.Playlist ? MprisLoopState.None : MprisLoopState.Playlist;
}
lastAction = {
"kind": name,
"id": playerIdValue,
"nativeActionRoute": nativeActionRoute
};
return JSON.stringify(stateObject(osd(name)));
}
function seek(position) {
const playerIdValue = activePlayer().id;
const nativePlayer = nativePlayerById(playerIdValue);
if (nativePlayer && nativePlayer.canSeek)
nativePlayer.position = Number(position);
lastAction = {
"kind": "seek",
"id": playerIdValue,
"position": position,
"nativeActionRoute": nativeActionRoute
};
return JSON.stringify(stateObject(osd(`seek ${position}`)));
}
function select(id) {
manualSelection = String(id);
lastAction = {
"kind": "select",
"id": manualSelection,
"nativeActionRoute": nativeActionRoute
};
return JSON.stringify(stateObject(osd(`select ${manualSelection}`)));
}
}