144 lines
4.2 KiB
QML
144 lines
4.2 KiB
QML
import Quickshell.Services.Pipewire
|
||
import QtQuick
|
||
import QtQuick.Layouts
|
||
import "../components"
|
||
|
||
// Volume mixer – a plain Item so it can live inside an expanding section border.
|
||
Item {
|
||
id: root
|
||
|
||
implicitWidth: 300
|
||
implicitHeight: mixerCol.implicitHeight + 24
|
||
|
||
PwObjectTracker {
|
||
objects: Pipewire.nodes.values
|
||
}
|
||
|
||
function safeVolume(node) {
|
||
if (!node || !node.ready || !node.audio) return 0
|
||
const v = node.audio.volume
|
||
return (v !== undefined && !isNaN(v)) ? v : 0
|
||
}
|
||
function setVolume(node, v) {
|
||
if (!node || !node.ready || !node.audio) return
|
||
node.audio.volume = Math.max(0, Math.min(1, v))
|
||
}
|
||
function volIcon(vol, muted) {
|
||
if (muted) return "\uf6a9"
|
||
if (vol > 0.6) return "\uf028"
|
||
if (vol > 0.2) return "\uf027"
|
||
return "\uf026"
|
||
}
|
||
|
||
ColumnLayout {
|
||
id: mixerCol
|
||
anchors { fill: parent; margins: 12 }
|
||
spacing: 4
|
||
|
||
Text {
|
||
text: "Output Devices"
|
||
color: Theme.textDim
|
||
font.pixelSize: 10
|
||
Layout.fillWidth: true
|
||
Layout.bottomMargin: 2
|
||
}
|
||
|
||
Repeater {
|
||
model: Pipewire.nodes
|
||
delegate: NodeRow {
|
||
required property var modelData
|
||
Layout.fillWidth: true
|
||
node: modelData
|
||
visible: modelData.isSink && !modelData.isStream && modelData.ready
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: "Applications"
|
||
color: Theme.textDim
|
||
font.pixelSize: 10
|
||
Layout.fillWidth: true
|
||
Layout.topMargin: 6
|
||
Layout.bottomMargin: 2
|
||
}
|
||
|
||
Repeater {
|
||
model: Pipewire.nodes
|
||
delegate: NodeRow {
|
||
required property var modelData
|
||
Layout.fillWidth: true
|
||
node: modelData
|
||
visible: modelData.isStream && !modelData.isSink && modelData.ready
|
||
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
|
||
}
|
||
}
|
||
}
|
||
|
||
component NodeRow: RowLayout {
|
||
id: row
|
||
required property var node
|
||
spacing: 8
|
||
implicitHeight: 28
|
||
|
||
Text {
|
||
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
|
||
font.family: "JetBrainsMono Nerd Font Mono"
|
||
font.pixelSize: 13
|
||
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (row.node.ready && row.node.audio)
|
||
row.node.audio.muted = !row.node.audio.muted
|
||
}
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: row.node.description || row.node.nickname || row.node.name || "?"
|
||
font.pixelSize: 11
|
||
color: Theme.textDim
|
||
Layout.preferredWidth: 90
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
Item {
|
||
Layout.fillWidth: true
|
||
implicitHeight: 16
|
||
|
||
Rectangle {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: parent.width
|
||
height: 3
|
||
radius: 2
|
||
color: Theme.progressTrack
|
||
|
||
Rectangle {
|
||
width: parent.width * Math.min(1, root.safeVolume(row.node))
|
||
height: parent.height
|
||
radius: parent.radius
|
||
color: Theme.accent
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
function seek(mx) { root.setVolume(row.node, mx / width) }
|
||
onClicked: seek(mouseX)
|
||
onPositionChanged: if (pressed) seek(mouseX)
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: Math.round(root.safeVolume(row.node) * 100) + "%"
|
||
font.pixelSize: 10
|
||
color: Theme.textDim
|
||
Layout.preferredWidth: 30
|
||
horizontalAlignment: Text.AlignRight
|
||
}
|
||
}
|
||
}
|