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