pragma ComponentBehavior: Bound import Quickshell import Quickshell.Services.Pipewire import Quickshell.Widgets import QtQuick import QtQuick.Layouts import QtQuick.Controls import ZShell.Config import qs.Components import qs.Daemons import qs.Helpers import qs.Services Item { id: root readonly property int panelRadius: Tokens.rounding.largeIncreased + Tokens.padding.smaller readonly property int rounding: panelRadius - Tokens.padding.smaller readonly property int topMargin: 0 required property var wrapper implicitHeight: vol.implicitHeight + Tokens.padding.smaller * 2 implicitWidth: 500 + Tokens.padding.smaller * 2 CustomRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top color: Colors.tPalette.m3surfaceContainer implicitHeight: parent.implicitHeight radius: root.rounding y: parent.y Behavior on implicitHeight { Anim { } } } VolumesTab { id: vol anchors.left: parent.left anchors.margins: Tokens.padding.smaller anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter } component VolumesTab: ColumnLayout { spacing: 12 CustomRect { Layout.fillWidth: true Layout.preferredHeight: 65 + Tokens.spacing.small * 2 Layout.topMargin: root.topMargin color: Colors.tPalette.m3surfaceContainer radius: root.rounding - vol.anchors.margins ColumnLayout { anchors.bottomMargin: Tokens.padding.small anchors.fill: parent anchors.leftMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small RowLayout { Layout.fillHeight: true Layout.fillWidth: true CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.fillWidth: true text: "Output volume" } CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight color: Qt.alpha(Colors.palette.m3onSurface, 0.7) font.bold: true text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) } } RowLayout { spacing: Tokens.spacing.largeIncreased CustomMouseArea { Layout.bottomMargin: Tokens.padding.small Layout.fillWidth: true Layout.preferredHeight: Tokens.padding.medium * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right bgColor: Audio.muted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer fgColor: Audio.muted ? Colors.palette.m3error : Colors.palette.m3primary implicitHeight: parent.height insetColor: Audio.muted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) insetIcon: Audio.muted || Audio.volume < 0.001 ? "volume_off" : "volume_up" value: Audio.volume Behavior on value { Anim { } } onInteraction: value => Audio.setVolume(value) } } CustomSwitch { Layout.bottomMargin: Tokens.padding.small checked: !Audio.muted onToggled: { const audio = Audio.sink?.audio; if (audio) audio.muted = !audio.muted; } } } } } CustomClippingRect { Layout.fillWidth: true Layout.preferredHeight: 65 + Tokens.spacing.small * 2 Layout.topMargin: root.topMargin color: Colors.tPalette.m3surfaceContainer radius: root.rounding - vol.anchors.margins PwNodePeakMonitor { id: sourcePeak node: Audio.source } CustomRect { id: sourcePeakFill anchors.bottom: parent.bottom anchors.left: parent.left anchors.top: parent.top color: Qt.alpha(Colors.palette.m3primary, 0.15) implicitWidth: parent.width * sourcePeak.peak Behavior on implicitWidth { Anim { } } } ColumnLayout { anchors.bottomMargin: Tokens.padding.small anchors.fill: parent anchors.leftMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small RowLayout { Layout.fillHeight: true Layout.fillWidth: true CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.fillWidth: true text: "Input volume" } CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight color: Qt.alpha(Colors.palette.m3onSurface, 0.7) font.bold: true text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`) } } RowLayout { spacing: Tokens.spacing.largeIncreased CustomMouseArea { Layout.bottomMargin: Tokens.padding.small Layout.fillWidth: true Layout.preferredHeight: Tokens.padding.medium * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right bgColor: Audio.sourceMuted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer fgColor: Audio.sourceMuted ? Colors.palette.m3error : Colors.palette.m3primary implicitHeight: parent.height insetColor: Audio.sourceMuted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) insetIcon: Audio.sourceMuted || Audio.sourceVolume < 0.001 ? "mic_off" : "mic" value: Audio.sourceVolume Behavior on value { Anim { } } onInteraction: value => Audio.setSourceVolume(value) } } CustomSwitch { Layout.bottomMargin: Tokens.padding.small checked: !Audio.sourceMuted onToggled: { const audio = Audio.source?.audio; if (audio) audio.muted = !audio.muted; } } } } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 Layout.topMargin: root.topMargin color: Colors.tPalette.m3outline visible: appTracks.model.length > 0 } Repeater { id: appTracks model: Audio.streams.filter(s => s.isSink) CustomClippingRect { id: appBox required property int index required property var modelData Layout.fillWidth: true Layout.preferredHeight: 65 + Tokens.spacing.small * 2 Layout.topMargin: root.topMargin color: Colors.tPalette.m3surfaceContainer radius: root.rounding - vol.anchors.margins PwNodePeakMonitor { id: peak node: appBox.modelData } CustomRect { id: peakFill anchors.bottom: parent.bottom anchors.left: parent.left anchors.top: parent.top color: Qt.alpha(Colors.palette.m3primary, 0.15) implicitWidth: parent.width * peak.peak Behavior on implicitWidth { Anim { } } } TextMetrics { id: metrics elide: Text.ElideRight elideWidth: root.width - 50 text: Audio.getStreamName(appBox.modelData) } ColumnLayout { anchors.bottomMargin: Tokens.padding.small anchors.fill: parent anchors.leftMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small RowLayout { Layout.fillHeight: true Layout.fillWidth: true CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.fillWidth: true elide: Text.ElideRight text: metrics.elidedText } CustomText { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight color: Qt.alpha(Colors.palette.m3onSurface, 0.7) font.bold: true text: qsTr("%1").arg(appBox.modelData.audio.muted ? qsTr("Muted") : `${Math.round(appBox.modelData.audio.volume * 100)}%`) } } RowLayout { spacing: Tokens.spacing.largeIncreased CustomMouseArea { Layout.bottomMargin: Tokens.padding.small Layout.fillWidth: true Layout.preferredHeight: Tokens.padding.medium * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right bgColor: appBox.modelData.audio.muted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer fgColor: appBox.modelData.audio.muted ? Colors.palette.m3error : Colors.palette.m3primary implicitHeight: parent.height insetColor: appBox.modelData.audio.muted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) insetIcon: appBox.modelData.audio.muted || appBox.modelData.audio.volume < 0.001 ? "volume_off" : "volume_up" value: appBox.modelData.audio.volume Behavior on value { Anim { } } onInteraction: value => Audio.setStreamVolume(appBox.modelData, value) } } CustomSwitch { Layout.bottomMargin: Tokens.padding.small checked: !appBox.modelData.audio.muted onToggled: { appBox.modelData.audio.muted = !appBox.modelData.audio.muted; } } } } } } } }