Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/AudioPanel.qml

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

Flickable {
    id: root

    required property Theme theme
    required property Audio audioService
    readonly property var panelState: audioService ? Util.parseJson(audioService.stateJson(), {}) : ({})
    readonly property var sinks: panelState.sinks || []
    readonly property var sources: panelState.sources || []
    readonly property var streams: panelState.streams || []
    property string expandedSink: ""
    property string expandedSource: ""

    function setVolume(id, ratio) {
        if (audioService)
            audioService.setVolume(id, Math.max(0, Math.min(1.5, ratio)));
    }

    function toggleMute(id) {
        if (audioService)
            audioService.toggleMute(id);
    }

    function selectSink(id) {
        if (audioService)
            audioService.setDefaultSink(id);
    }

    function selectSource(id) {
        if (audioService)
            audioService.setDefaultSource(id);
    }

    function roleColor(role) {
        if (role === "warning")
            return theme.warning;
        if (role === "error")
            return theme.error;
        if (role === "ui.minor")
            return theme.ui.minor;
        return theme.ui.accent;
    }

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

    component DeviceSection: Column {
        id: section

        required property bool input
        required property var rows
        readonly property string expanded: input ? root.expandedSource : root.expandedSink

        function expand(id) {
            const next = expanded === id ? "" : id;
            if (input)
                root.expandedSource = next;
            else
                root.expandedSink = next;
        }

        spacing: 6

        StableListModel {
            id: devices
            rows: section.rows
        }

        NuguText {
            theme: root.theme
            variant: "title"
            text: section.input ? "input" : "output"
        }

        NuguText {
            theme: root.theme
            variant: "meta"
            visible: devices.count === 0
            text: section.input ? "no input devices" : "no output devices"
        }

        Repeater {
            model: devices

            delegate: Column {
                id: deviceRow

                required property var rowData
                readonly property bool revealed: section.expanded === rowData.id

                width: section.width
                spacing: 4

                PopupCard {
                    theme: root.theme
                    padding: 0
                    active: deviceRow.rowData.isDefault
                    revealed: deviceRow.revealed
                    interactive: true
                    Accessible.name: `${deviceRow.rowData.name} options`
                    Accessible.description: deviceRow.rowData.isDefault ? "default device" : ""
                    tooltip: `${deviceRow.rowData.name}${deviceRow.rowData.isDefault ? "\ndefault device" : ""}\n${deviceRow.revealed ? "hide" : "show"} device options`
                    onControlFocusedChanged: if (controlFocused)
                        Util.revealIn(this, root)
                    onClicked: section.expand(deviceRow.rowData.id)
                    width: parent.width
                    height: 48

                    Text {
                        id: deviceGlyph

                        anchors.left: parent.left
                        anchors.leftMargin: 12
                        anchors.verticalCenter: parent.verticalCenter
                        font.family: "Symbols Nerd Font Mono"
                        font.pixelSize: 18
                        text: deviceRow.rowData.glyph
                        color: root.roleColor(deviceRow.rowData.role)
                        opacity: deviceRow.rowData.muted ? 0.5 : 1
                    }

                    NuguText {
                        theme: root.theme
                        text: `${deviceRow.rowData.name}${deviceRow.rowData.isDefault ? " · default" : ""}`
                        anchors.left: deviceGlyph.right
                        anchors.leftMargin: 10
                        anchors.right: deviceTrack.left
                        anchors.rightMargin: 10
                        anchors.verticalCenter: parent.verticalCenter
                        elide: Text.ElideRight
                        opacity: deviceRow.rowData.muted ? 0.5 : 1
                    }

                    NuguText {
                        id: trailing

                        anchors.right: parent.right
                        anchors.rightMargin: 12
                        anchors.verticalCenter: parent.verticalCenter
                        theme: root.theme
                        variant: "meta"
                        width: 48
                        horizontalAlignment: Text.AlignRight
                        text: deviceRow.rowData.muted ? "muted" : `${deviceRow.rowData.percent}%`
                        color: root.roleColor(deviceRow.rowData.muted ? "ui.minor" : deviceRow.rowData.role)
                    }

                    PopupSlider {
                        id: deviceTrack

                        anchors.right: trailing.left
                        anchors.rightMargin: 10
                        anchors.verticalCenter: parent.verticalCenter
                        theme: root.theme
                        width: 120
                        Accessible.name: `${deviceRow.rowData.name} ${section.input ? "input" : "output"} volume`
                        onActiveFocusChanged: if (activeFocus)
                            Util.revealIn(this, root)
                        value: deviceRow.rowData.muted ? 0 : deviceRow.rowData.percent / 150
                        onMoved: value => root.setVolume(deviceRow.rowData.id, value * 1.5)
                    }
                }

                Row {
                    visible: deviceRow.revealed
                    width: parent.width
                    height: visible ? 34 : 0
                    spacing: 8

                    PopupButton {
                        theme: root.theme
                        text: deviceRow.rowData.isDefault ? "default" : "set default"
                        enabled: !deviceRow.rowData.isDefault
                        checked: deviceRow.rowData.isDefault
                        onControlFocusedChanged: if (controlFocused)
                            Util.revealIn(this, root)
                        onClicked: {
                            section.expand(deviceRow.rowData.id);
                            if (section.input)
                                root.selectSource(deviceRow.rowData.id);
                            else
                                root.selectSink(deviceRow.rowData.id);
                        }
                    }

                    PopupButton {
                        theme: root.theme
                        text: deviceRow.rowData.muted ? "unmute" : "mute"
                        checked: deviceRow.rowData.muted
                        accessibleName: `${text} ${deviceRow.rowData.name}`
                        onControlFocusedChanged: if (controlFocused)
                            Util.revealIn(this, root)
                        onClicked: root.toggleMute(deviceRow.rowData.id)
                    }
                }
            }
        }
    }

    Column {
        id: audioCol

        width: root.width
        spacing: 12

        DeviceSection {
            objectName: "outputSection"
            width: parent.width
            input: false
            rows: root.sinks
        }

        DeviceSection {
            objectName: "inputSection"
            width: parent.width
            input: true
            rows: root.sources
        }
    }
}