Files

170 lines
6.3 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Services
import qs.DCommon.Widgets
import "../../../Common/QmlUtils.js" as QmlUtils
Item {
id: root
implicitHeight: column.height
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property string title: I18n.tr("Audio Output")
readonly property var pinnedOutputs: QmlUtils.normalizePinList((CacheData.audioOutputDevicePins || {})["preferredOutput"])
readonly property Item headerActions: CcSettingsButton {
settingsTab: "audio"
}
signal showPortSelector(var node)
Component.onCompleted: {
AudioService.refreshSinkPorts();
AudioService.refreshCards();
}
function togglePin(name) {
CacheData.set("audioOutputDevicePins", QmlUtils.togglePinEntry(CacheData.audioOutputDevicePins, "preferredOutput", name, CcMetrics.maxPins));
}
DFlickable {
anchors.fill: parent
contentHeight: column.height
clip: true
Column {
id: column
width: parent.width
spacing: CcMetrics.detailContentGap
CcGroup {
AudioSliderRow {
node: AudioService.sink
maxVolume: AudioService.sinkMaxVolume
playFeedback: true
}
}
CcSectionLabel {
text: I18n.tr("Audio Devices")
}
CcGroup {
Repeater {
model: ScriptModel {
values: {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
const nodes = AudioService.allNodes.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
const pinnedList = root.pinnedOutputs;
return nodes.sort((a, b) => {
const aPinned = pinnedList.indexOf(a.name);
const bPinned = pinnedList.indexOf(b.name);
if (aPinned !== -1 || bPinned !== -1) {
if (aPinned === -1)
return 1;
if (bPinned === -1)
return -1;
return aPinned - bPinned;
}
if (a === AudioService.sink && b !== AudioService.sink)
return -1;
if (b === AudioService.sink && a !== AudioService.sink)
return 1;
return 0;
});
}
}
CcListRow {
id: deviceRow
required property var modelData
iconName: AudioService.sinkIcon(modelData)
title: AudioService.displayName(modelData)
subtitle: active ? I18n.tr("Active") : I18n.tr("Available")
active: modelData === AudioService.sink
clickable: true
onClicked: {
if (modelData?.name)
AudioService.setDefaultSinkByName(modelData.name);
}
DActionButton {
anchors.verticalCenter: parent.verticalCenter
visible: AudioService.sinkHasMultiplePorts(deviceRow.modelData)
buttonSize: Theme.buttonHeightXS
iconSize: Theme.iconSizeMedium
iconName: "tune"
iconColor: Theme.surfaceText
tooltipText: I18n.tr("Port Selection", "audio output port selector button tooltip")
onClicked: root.showPortSelector(deviceRow.modelData)
}
CcPinChip {
anchors.verticalCenter: parent.verticalCenter
pinned: root.pinnedOutputs.includes(deviceRow.modelData.name)
onToggled: root.togglePin(deviceRow.modelData.name)
}
}
}
Repeater {
model: ScriptModel {
values: AudioService.switchableOutputPorts
}
CcListRow {
required property var modelData
iconName: modelData.icon
title: modelData.title
subtitle: modelData.cardDescription
clickable: true
onClicked: AudioService.activateOutputPort(modelData, (ok, message) => ToastService.showToast(message, ok ? ToastService.levelInfo : ToastService.levelError))
}
}
}
CcSectionLabel {
text: I18n.tr("Playback", "section label above per-app audio playback streams")
visible: playbackGroup.visible
}
CcGroup {
id: playbackGroup
visible: playbackRepeater.count > 0
Repeater {
id: playbackRepeater
model: ScriptModel {
values: AudioService.pipewireNodes.filter(node => node.audio && node.isSink && node.isStream && !AudioService.isOwnStream(node))
}
CcListRow {
id: streamRow
required property var modelData
title: AudioService.displayName(modelData) + ": " + (modelData?.properties?.["media.name"] || "")
body: AudioSliderRow {
node: streamRow.modelData
sliderLabel: streamRow.title
playFeedback: true
}
}
}
}
}
}
}