import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.Common import qs.Services import qs.Widgets DankFlickable { id: deviceList property bool isSink: true readonly property var activeNode: isSink ? AudioService.sink : AudioService.source readonly property var deviceNodes: { const hidden = isSink ? (SessionData.hiddenOutputDeviceNames ?? []) : (SessionData.hiddenInputDeviceNames ?? []); const nodes = Pipewire.nodes.values.filter(node => { if (!node.audio || node.isStream) return false; if (node.isSink !== isSink) return false; return !hidden.includes(node.name); }); const active = activeNode; return [...nodes].sort((a, b) => { if (a === active) return -1; if (b === active) return 1; return AudioService.displayName(a).localeCompare(AudioService.displayName(b)); }); } function activate(node) { if (!node || !node.name) return; if (deviceList.isSink) AudioService.setDefaultSinkByName(node.name); else AudioService.setDefaultSourceByName(node.name); } clip: true contentHeight: deviceColumn.height contentWidth: width Column { id: deviceColumn width: deviceList.width spacing: Theme.spacingXS Repeater { model: ScriptModel { values: deviceList.deviceNodes } delegate: Rectangle { id: deviceItem required property var modelData readonly property bool isActive: modelData === deviceList.activeNode width: deviceColumn.width height: 46 radius: Theme.cornerRadius color: isActive ? Theme.primarySelected : (deviceArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)) border.color: isActive ? Theme.primary : Theme.outlineLight border.width: isActive ? 2 : Theme.layerOutlineWidth DankIcon { id: deviceIcon anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter name: deviceList.isSink ? AudioService.sinkIcon(deviceItem.modelData) : "mic" size: Theme.iconSize - 4 color: deviceItem.isActive ? Theme.primary : Theme.surfaceText } DankIcon { id: activeCheck anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter name: "check_circle" size: Theme.iconSize - 6 color: Theme.primary visible: deviceItem.isActive } Column { anchors.left: deviceIcon.right anchors.leftMargin: Theme.spacingS anchors.right: activeCheck.visible ? activeCheck.left : parent.right anchors.rightMargin: Theme.spacingS anchors.verticalCenter: parent.verticalCenter spacing: 1 StyledText { width: parent.width text: AudioService.displayName(deviceItem.modelData) font.pixelSize: Theme.fontSizeMedium font.weight: deviceItem.isActive ? Font.Medium : Font.Normal color: Theme.surfaceText elide: Text.ElideRight } StyledText { width: parent.width text: AudioService.subtitle(deviceItem.modelData.name) font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText elide: Text.ElideRight visible: text !== "" } } MouseArea { id: deviceArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: deviceList.activate(deviceItem.modelData) } PwObjectTracker { objects: [deviceItem.modelData] } } } StyledText { width: deviceColumn.width text: deviceList.isSink ? "No output devices found" : "No input devices found" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText horizontalAlignment: Text.AlignHCenter visible: deviceList.deviceNodes.length === 0 } } }