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 } } }