Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/MediaPanel.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import "Util.js" as Util

Flickable {
    id: root

    required property Theme theme
    required property Media mediaService
    readonly property var panelState: mediaService ? Util.parseJson(mediaService.stateJson(), {}) : ({})
    readonly property var player: panelState.activePlayer || ({})
    readonly property var players: panelState.players || []
    readonly property bool hasMedia: players.length > 0
    readonly property var panel: panelState.mediaPanelContract || ({})
    // The existing service snapshot omits transport capabilities. Read them from
    // its native-player lookup, without introducing another command route.
    readonly property var nativePlayer: mediaService && hasMedia ? mediaService.nativePlayerById(player.id) : null
    readonly property bool canSeek: !!(nativePlayer && nativePlayer.canControl && player.canSeek && Number(player.length) > 0)
    readonly property var transportActions: ["shuffle", "previous", "playPause", "next", "repeat"]

    function mmss(seconds) {
        const total = Math.max(0, Math.round(Number(seconds || 0)));
        const minutes = Math.floor(total / 60);
        const rest = String(total % 60).padStart(2, "0");
        return `${minutes}:${rest}`;
    }

    function actionEnabled(kind) {
        if (!nativePlayer || !nativePlayer.canControl)
            return false;
        if (kind === "shuffle")
            return !!player.canShuffle;
        if (kind === "repeat")
            return !!player.canRepeat;
        if (kind === "previous")
            return !!nativePlayer.canGoPrevious;
        if (kind === "next")
            return !!nativePlayer.canGoNext;
        if (kind === "playPause")
            return !!nativePlayer.canTogglePlaying;
        return false;
    }

    function runAction(kind) {
        if (mediaService && actionEnabled(kind))
            mediaService.action(kind);
    }

    function seekByRatio(ratio) {
        const length = Math.max(0, Number(player.length || 0));
        if (mediaService && canSeek)
            mediaService.seek(Math.round(Math.max(0, Math.min(1, ratio)) * length));
    }

    function selectPlayer(id) {
        if (mediaService)
            mediaService.select(id);
    }

    function actionIcon(kind) {
        if (kind === "shuffle")
            return "\uF074";
        if (kind === "previous")
            return "󰒮";
        if (kind === "playPause")
            return root.player.playbackState === "playing" ? "\uF04C" : "\uF04B";
        if (kind === "next")
            return "󰒭";
        if (kind === "repeat")
            return "\uF01E";
        return "\uF001";
    }

    function actionActive(kind) {
        return kind === "shuffle" && root.player.shuffle || kind === "repeat" && root.player.repeat !== "none";
    }

    function actionLabel(kind) {
        if (kind === "playPause")
            return root.player.playbackState === "playing" ? "pause" : "play";
        if (kind === "previous")
            return "previous track";
        if (kind === "next")
            return "next track";
        return kind;
    }

    width: parent ? parent.width : 0
    contentWidth: width
    contentHeight: mediaCol.height
    clip: true
    interactive: contentHeight > height
    boundsBehavior: Flickable.StopAtBounds
    flickableDirection: Flickable.VerticalFlick

    StableListModel {
        id: playerModel
        rows: root.players
    }

    Column {
        id: mediaCol

        width: root.width
        spacing: 12

        PopupCard {
            id: nowPlayingCard

            theme: root.theme
            padding: 0
            objectName: "coverDetailsProgressTransport"
            visible: root.hasMedia
            width: parent.width
            height: 252

            Rectangle {
                id: coverCard

                anchors.left: parent.left
                anchors.top: parent.top
                anchors.margins: 12
                width: 104
                height: 104
                radius: 12
                color: root.theme.ui.backdrop
                clip: true

                Image {
                    id: coverArt

                    anchors.fill: parent
                    visible: !!root.player.artAvailable && status === Image.Ready
                    source: root.player.cover || ""
                    fillMode: Image.PreserveAspectCrop
                    asynchronous: true
                }

                Text {
                    anchors.centerIn: parent
                    visible: !coverArt.visible
                    font.family: "Symbols Nerd Font Mono"
                    font.pixelSize: 36
                    color: root.theme.ui.accent
                    text: "\uF001"
                }
            }

            Column {
                anchors.left: coverCard.right
                anchors.leftMargin: 16
                anchors.right: parent.right
                anchors.rightMargin: 12
                anchors.verticalCenter: coverCard.verticalCenter
                spacing: 6

                NuguText {
                    theme: root.theme
                    variant: "title"
                    text: root.player.title || root.player.identity || "untitled"
                    width: parent.width
                    elide: Text.ElideRight
                }

                NuguText {
                    theme: root.theme
                    text: root.player.artist || "unknown artist"
                    width: parent.width
                    elide: Text.ElideRight
                }

                NuguText {
                    theme: root.theme
                    variant: "meta"
                    text: root.player.album || ""
                    visible: text.length > 0
                    width: parent.width
                    elide: Text.ElideRight
                }

                Row {
                    width: parent.width
                    spacing: 8

                    PopupBadge {
                        id: playbackBadge

                        theme: root.theme
                        text: root.player.playbackState || "stopped"
                        role: root.player.playbackState === "playing" ? "active" : "normal"
                    }

                    NuguText {
                        theme: root.theme
                        variant: "meta"
                        text: root.player.identity || "mpris"
                        width: parent.width - playbackBadge.width - 8
                        anchors.verticalCenter: parent.verticalCenter
                        elide: Text.ElideRight
                    }
                }
            }

            Item {
                id: progressBlock

                anchors.left: parent.left
                anchors.right: parent.right
                anchors.margins: 12
                anchors.bottom: transportRow.top
                anchors.bottomMargin: 12
                height: 44

                NuguText {
                    anchors.left: parent.left
                    anchors.top: parent.top
                    theme: root.theme
                    variant: "mono"
                    text: root.mmss(root.player.position)
                }

                NuguText {
                    anchors.right: parent.right
                    anchors.top: parent.top
                    theme: root.theme
                    variant: "mono"
                    text: root.mmss(root.player.length)
                }

                PopupSlider {
                    objectName: "mpris-progress"
                    anchors.left: parent.left
                    anchors.right: parent.right
                    anchors.bottom: parent.bottom
                    theme: root.theme
                    enabled: root.canSeek
                    Accessible.name: `playback position for ${root.player.title || root.player.identity || "media"}`
                    value: Number(root.player.progress || 0)
                    onMoved: value => root.seekByRatio(value)
                }
            }

            Row {
                id: transportRow

                objectName: "transportActions"
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 12
                spacing: 8

                Repeater {
                    model: root.transportActions

                    delegate: PopupButton {
                        required property string modelData
                        readonly property bool isPrimary: modelData === "playPause"

                        theme: root.theme
                        width: 40
                        height: 34
                        icon: root.actionIcon(modelData)
                        accessibleName: root.actionLabel(modelData)
                        tooltip: enabled ? accessibleName : `${accessibleName} unavailable for this player`
                        enabled: root.actionEnabled(modelData)
                        role: isPrimary ? "primary" : "normal"
                        checked: root.actionActive(modelData)
                        onClicked: root.runAction(modelData)
                    }
                }
            }
        }

        Flow {
            objectName: "playerSelectorRail"
            width: parent.width
            spacing: 6
            visible: root.players.length > 1

            Repeater {
                model: playerModel

                delegate: PopupButton {
                    required property var rowData

                    theme: root.theme
                    width: Math.min(root.width, implicitWidth)
                    text: rowData.identity || rowData.id
                    accessibleName: `select ${text}`
                    checked: rowData.id === root.player.id
                    onClicked: root.selectPlayer(rowData.id)
                }
            }
        }

        Item {
            objectName: "mediaEmptyState"
            visible: !root.hasMedia
            width: parent.width
            height: 56

            Text {
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                font.family: "Symbols Nerd Font Mono"
                font.pixelSize: 26
                color: root.theme.ui.minor
                text: "\uF001"
                Accessible.ignored: true
            }

            Column {
                anchors.left: parent.left
                anchors.leftMargin: 40
                anchors.right: parent.right
                anchors.verticalCenter: parent.verticalCenter
                spacing: 3

                NuguText {
                    theme: root.theme
                    variant: "title"
                    text: "nothing playing"
                }

                NuguText {
                    width: parent.width
                    theme: root.theme
                    variant: "meta"
                    text: "play media in an app to control it here"
                    color: root.theme.ui.minor
                    wrapMode: Text.WordWrap
                }
            }
        }
    }
}