import QtQuick import Quickshell.Services.Pipewire import "../services" as S Column { id: root required property var sink required property var sinkList required property var streamList required property color accentColor property real volume: sink?.audio?.volume ?? 0 property bool muted: sink?.audio?.muted ?? false readonly property string volumeIcon: muted ? "\uF026" : (volume > 0.5 ? "\uF028" : (volume > 0 ? "\uF027" : "\uF026")) readonly property color volumeColor: muted ? S.Theme.base04 : root.accentColor // Slider row Item { width: root.width height: 36 Text { id: muteIcon anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: root.volumeIcon color: root.volumeColor font.pixelSize: S.Theme.fontSize + 2 font.family: S.Theme.iconFontFamily HoverHandler { cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: if (root.sink?.audio) root.sink.audio.muted = !root.sink.audio.muted } } Item { id: slider anchors.left: muteIcon.right anchors.leftMargin: 8 anchors.right: volLabel.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter height: 6 Rectangle { anchors.fill: parent color: S.Theme.base02 radius: 3 } Rectangle { width: parent.width * Math.min(1, Math.max(0, root.volume)) height: parent.height color: root.volumeColor radius: 3 Behavior on width { NumberAnimation { duration: 80 } } } MouseArea { anchors.fill: parent anchors.margins: -6 cursorShape: Qt.PointingHandCursor onPressed: mouse => _setVol(mouse) onPositionChanged: mouse => { if (pressed) _setVol(mouse); } function _setVol(mouse) { if (!root.sink?.audio) return; root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); } } } Text { id: volLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: Math.round(root.volume * 100) + "%" color: root.muted ? S.Theme.base04 : S.Theme.base05 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily width: 30 } } // Device + stream list Column { id: deviceList width: root.width // Output devices - only shown when more than one exists Column { visible: root.sinkList.length > 1 width: parent.width Rectangle { width: parent.width - 16 height: 1 anchors.horizontalCenter: parent.horizontalCenter color: S.Theme.base03 } Text { width: parent.width height: 24 verticalAlignment: Text.AlignVCenter leftPadding: 12 text: "Output Devices" color: root.accentColor font.pixelSize: S.Theme.fontSize - 1 font.family: S.Theme.fontFamily } Repeater { model: root.sinkList delegate: Item { required property var modelData width: root.width height: 28 readonly property bool _active: modelData === root.sink Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: deviceHover.hovered ? S.Theme.base02 : "transparent" radius: S.Theme.radius } Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: modelData.description || modelData.name || "Unknown" color: parent._active ? root.accentColor : S.Theme.base05 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily font.bold: parent._active elide: Text.ElideRight } HoverHandler { id: deviceHover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: Pipewire.preferredDefaultAudioSink = modelData } } } } // Streams section Rectangle { visible: root.streamList.length > 0 width: parent.width - 16 height: visible ? 1 : 0 anchors.horizontalCenter: parent.horizontalCenter color: S.Theme.base03 } Text { visible: root.streamList.length > 0 width: parent.width height: visible ? 24 : 0 verticalAlignment: Text.AlignVCenter leftPadding: 12 text: "Applications" color: root.accentColor font.pixelSize: S.Theme.fontSize - 1 font.family: S.Theme.fontFamily } Repeater { model: root.streamList delegate: Item { id: streamEntry required property var modelData width: root.width height: 32 readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" readonly property real _vol: modelData.audio?.volume ?? 0 readonly property bool _muted: modelData.audio?.muted ?? false Text { id: streamIcon anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: streamEntry._muted ? "\uF026" : "\uF028" color: streamEntry._muted ? S.Theme.base04 : S.Theme.base05 font.pixelSize: S.Theme.fontSize font.family: S.Theme.iconFontFamily HoverHandler { cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: if (streamEntry.modelData.audio) streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted } } Text { id: streamName anchors.left: streamIcon.right anchors.leftMargin: 6 anchors.verticalCenter: parent.verticalCenter text: streamEntry._appName color: S.Theme.base05 font.pixelSize: S.Theme.fontSize - 1 font.family: S.Theme.fontFamily elide: Text.ElideRight width: 70 } Item { id: streamSlider anchors.left: streamName.right anchors.leftMargin: 6 anchors.right: streamVol.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter height: 4 Rectangle { anchors.fill: parent color: S.Theme.base02 radius: 2 } Rectangle { width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) height: parent.height color: streamEntry._muted ? S.Theme.base04 : root.accentColor radius: 2 } MouseArea { anchors.fill: parent anchors.margins: -6 cursorShape: Qt.PointingHandCursor onPressed: mouse => _set(mouse) onPositionChanged: mouse => { if (pressed) _set(mouse); } function _set(mouse) { if (!streamEntry.modelData.audio) return; streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); } } } Text { id: streamVol anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: Math.round(streamEntry._vol * 100) + "%" color: streamEntry._muted ? S.Theme.base04 : S.Theme.base05 font.pixelSize: S.Theme.fontSize - 1 font.family: S.Theme.fontFamily width: 28 } } } // Bottom padding Item { width: 1 height: 4 } } }