import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.Common import qs.Services import qs.Widgets DankFlickable { id: mixer readonly property var streams: Pipewire.nodes.values.filter(node => node.audio && node.isSink && node.isStream) readonly property int streamCount: streams.length function appName(node) { if (!node) return ""; const props = node.properties || {}; return props["application.name"] || AudioService.displayName(node) || "Unknown"; } function mediaName(node) { const props = (node && node.properties) ? node.properties : {}; return props["media.name"] || ""; } function appIconValue(node) { const props = (node && node.properties) ? node.properties : {}; const candidates = [props["application.icon-name"], props["application.process.binary"], props["application.name"]]; for (const candidate of candidates) { if (!candidate) continue; const entry = DesktopEntries.heuristicLookup(String(candidate)); if (entry && entry.icon) return entry.icon; } return "material:graphic_eq"; } clip: true contentHeight: streamColumn.height contentWidth: width Column { id: streamColumn width: mixer.width spacing: Theme.spacingS Repeater { model: ScriptModel { values: mixer.streams } delegate: Rectangle { id: streamItem required property var modelData width: streamColumn.width height: 86 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency) border.color: Theme.outlineLight border.width: Theme.layerOutlineWidth AppIconRenderer { id: streamIcon anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.top: parent.top anchors.topMargin: Theme.spacingM width: Theme.iconSize height: Theme.iconSize iconValue: mixer.appIconValue(streamItem.modelData) iconSize: Theme.iconSize fallbackText: mixer.appName(streamItem.modelData).charAt(0).toUpperCase() } Column { anchors.left: streamIcon.right anchors.leftMargin: Theme.spacingS anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.top: parent.top anchors.topMargin: Theme.spacingM spacing: 1 StyledText { width: parent.width text: mixer.appName(streamItem.modelData) font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium color: Theme.surfaceText elide: Text.ElideRight } StyledText { width: parent.width text: mixer.mediaName(streamItem.modelData) font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText elide: Text.ElideRight visible: text !== "" } } GameBarVolumeRow { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.leftMargin: Theme.spacingS anchors.rightMargin: Theme.spacingM anchors.bottomMargin: Theme.spacingXS node: streamItem.modelData maxPercent: 100 activeIcon: "volume_up" mutedIcon: "volume_off" } PwObjectTracker { objects: [streamItem.modelData] } } } Item { width: streamColumn.width height: 120 visible: mixer.streamCount === 0 Column { anchors.centerIn: parent spacing: Theme.spacingS DankIcon { anchors.horizontalCenter: parent.horizontalCenter name: "music_off" size: Theme.iconSizeLarge color: Theme.surfaceVariantText } StyledText { anchors.horizontalCenter: parent.horizontalCenter text: "No apps are playing audio" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } } } } }