Krolyxon
Branch master

Branches

Refs →

aurora.configquickshellauroraAudioPage.qml

158 lines · 6.3 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import Quickshell.Services.Pipewire
4
5// Control panel > sound (also the settings app's sound section): output / input levels, devices, per-app volume.
6Flickable {
7 id: flick
8 property bool cards: false // the settings app: groups on cards
9 clip: true
10 contentWidth: width
11 contentHeight: col.implicitHeight
12 implicitHeight: col.implicitHeight
13 boundsBehavior: Flickable.StopAtBounds
14
15 function mediaClass(n) {
16 return (n.properties && n.properties["media.class"]) ? String(n.properties["media.class"]) : ""
17 }
18 function label(n) { return n.description || n.nickname || n.name }
19
20 // kind: "sink" | "source" | "stream"
21 function nodes(kind) {
22 var l = Pipewire.nodes.values, out = []
23 for (var i = 0; i < l.length; i++) {
24 var n = l[i]
25 if (!n.audio) continue
26 if (kind === "sink" && !n.isStream && n.isSink) out.push(n)
27 else if (kind === "source" && !n.isStream && !n.isSink) out.push(n)
28 else if (kind === "stream" && n.isStream && mediaClass(n).indexOf("Output") >= 0) out.push(n)
29 }
30 return out
31 }
32 function sinkIcon(n) {
33 var s = (n.name + " " + n.description).toLowerCase()
34 return (s.indexOf("bluez") >= 0 || s.indexOf("head") >= 0) ? Theme.iHeadphones : Theme.iSpeaker
35 }
36
37 readonly property var sinks: nodes("sink")
38 readonly property var sources: nodes("source")
39 readonly property var streams: nodes("stream")
40 readonly property var out: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null
41 readonly property var inp: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null
42
43 ColumnLayout {
44 id: col
45 width: flick.width
46 spacing: flick.cards ? 22 : 6
47
48 Group {
49 title: "levels"
50 keywords: "volume mute microphone mic"
51 flat: !flick.cards
52 spacing: 6
53 gap: 6
54 SliderRow {
55 Layout.fillWidth: true
56 Layout.topMargin: 4
57 icon: Theme.volIcon(flick.out ? flick.out.volume : 0, flick.out ? flick.out.muted : true)
58 value: flick.out ? flick.out.volume : 0
59 muted: flick.out ? flick.out.muted : false
60 onMoved: v => { if (flick.out) flick.out.volume = v }
61 onIconClicked: if (flick.out) flick.out.muted = !flick.out.muted
62 }
63 SliderRow {
64 Layout.fillWidth: true
65 Layout.topMargin: 6
66 icon: flick.inp && !flick.inp.muted ? Theme.iMic : Theme.iMicOff
67 value: flick.inp ? flick.inp.volume : 0
68 muted: flick.inp ? flick.inp.muted : false
69 onMoved: v => { if (flick.inp) flick.inp.volume = v }
70 onIconClicked: if (flick.inp) flick.inp.muted = !flick.inp.muted
71 }
72 }
73 Group {
74 title: "output"
75 keywords: "speakers headphones hdmi default device sink"
76 count: String(flick.sinks.length)
77 flat: !flick.cards
78 spacing: 6
79 gap: 6
80 Layout.topMargin: flick.cards ? 0 : 14
81 Repeater {
82 model: flick.sinks
83 delegate: ListRow {
84 required property var modelData
85 icon: flick.sinkIcon(modelData)
86 title: flick.label(modelData)
87 subtitle: current ? "default" : ""
88 current: Pipewire.defaultAudioSink === modelData
89 trailing: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
90 onClicked: Pipewire.preferredDefaultAudioSink = modelData
91 }
92 }
93 }
94 Group {
95 title: "input"
96 keywords: "microphone mic default device source"
97 count: String(flick.sources.length)
98 flat: !flick.cards
99 spacing: 6
100 gap: 6
101 Layout.topMargin: flick.cards ? 0 : 14
102 Repeater {
103 model: flick.sources
104 delegate: ListRow {
105 required property var modelData
106 icon: Theme.iMic
107 title: flick.label(modelData)
108 subtitle: current ? "default" : ""
109 current: Pipewire.defaultAudioSource === modelData
110 trailing: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
111 onClicked: Pipewire.preferredDefaultAudioSource = modelData
112 }
113 }
114 }
115 Group {
116 title: "apps"
117 keywords: "mixer per-app app volume streams"
118 count: String(flick.streams.length)
119 flat: !flick.cards
120 spacing: 6
121 gap: 6
122 Layout.topMargin: flick.cards ? 0 : 14
123 Lb {
124 visible: flick.streams.length === 0
125 Layout.leftMargin: 6
126 text: "no app is playing"
127 color: Theme.mute
128 }
129 Repeater {
130 model: flick.streams
131 delegate: ColumnLayout {
132 required property var modelData
133 Layout.fillWidth: true
134 Layout.topMargin: 4
135 spacing: 6
136 Lb {
137 Layout.leftMargin: 6
138 Layout.fillWidth: true
139 elide: Text.ElideRight
140 px: 10
141 text: (modelData.properties && modelData.properties["application.name"])
142 ? modelData.properties["application.name"] : flick.label(modelData)
143 }
144 SliderRow {
145 Layout.fillWidth: true
146 implicitHeight: 44
147 icon: Theme.volIcon(modelData.audio.volume, modelData.audio.muted)
148 value: modelData.audio.volume
149 muted: modelData.audio.muted
150 onMoved: v => modelData.audio.volume = v
151 onIconClicked: modelData.audio.muted = !modelData.audio.muted
152 }
153 }
154 }
155 }
156 Item { Layout.preferredHeight: 4 }
157 }
158}