Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Media.qml

Raw
import 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}`)));
    }
}