import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services.Media import qs.Widgets // Audio controls card: output and input volume controls NBox { id: root property real localOutputVolume: 0 property bool localOutputVolumeChanging: false property int lastSinkId: -1 property real localInputVolume: 0 property bool localInputVolumeChanging: false property int lastSourceId: -1 readonly property bool outputVolumeGuard: outputVolumeSlider.sliderActive || localOutputVolumeChanging readonly property bool inputVolumeGuard: inputVolumeSlider.sliderActive || localInputVolumeChanging Component.onCompleted: { var vol = AudioService.volume; localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; var inputVol = AudioService.inputVolume; localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0; if (AudioService.sink) { lastSinkId = AudioService.sink.id; } if (AudioService.source) { lastSourceId = AudioService.source.id; } } // Reset local volume when device changes - use current device's volume Connections { target: AudioService function onSinkChanged() { if (AudioService.sink) { const newSinkId = AudioService.sink.id; if (newSinkId !== lastSinkId) { lastSinkId = newSinkId; // Immediately set local volume to current device's volume var vol = AudioService.volume; localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } else { lastSinkId = -1; localOutputVolume = 0; } } } Connections { target: AudioService function onSourceChanged() { if (AudioService.source) { const newSourceId = AudioService.source.id; if (newSourceId !== lastSourceId) { lastSourceId = newSourceId; // Immediately set local volume to current device's volume var vol = AudioService.inputVolume; localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } else { lastSourceId = -1; localInputVolume = 0; } } } // Timer to debounce volume changes // Only sync if the device hasn't changed (check by comparing IDs) Timer { interval: 100 running: true repeat: true onTriggered: { // Only sync if sink hasn't changed if (AudioService.sink && AudioService.sink.id === lastSinkId) { if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) { AudioService.setVolume(localOutputVolume); } } // Only sync if source hasn't changed if (AudioService.source && AudioService.source.id === lastSourceId) { if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) { AudioService.setInputVolume(localInputVolume); } } } } Connections { target: AudioService function onVolumeChanged() { if (!outputVolumeGuard && !AudioService.isSettingOutputVolume && AudioService.sink && AudioService.sink.id === lastSinkId) { var vol = AudioService.volume; localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } } Connections { target: AudioService function onInputVolumeChanged() { if (!inputVolumeGuard && !AudioService.isSettingInputVolume && AudioService.source && AudioService.source.id === lastSourceId) { var vol = AudioService.inputVolume; localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } } Connections { target: outputVolumeSlider function onSliderActiveChanged() { if (!outputVolumeSlider.sliderActive && AudioService.sink && AudioService.sink.id === lastSinkId) { var vol = AudioService.volume; localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } } Connections { target: inputVolumeSlider function onSliderActiveChanged() { if (!inputVolumeSlider.sliderActive && AudioService.source && AudioService.source.id === lastSourceId) { var vol = AudioService.inputVolume; localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; } } } RowLayout { anchors.fill: parent anchors.margins: Style.marginM spacing: Style.marginM // Output Volume Section ColumnLayout { spacing: Style.marginXXS Layout.fillWidth: true Layout.preferredWidth: 0 opacity: AudioService.sink ? 1.0 : 0.5 enabled: AudioService.sink // Output Volume Header RowLayout { Layout.fillWidth: true spacing: Style.marginXS NIconButton { icon: AudioService.muted ? "volume-off" : "volume-high" baseSize: Style.baseWidgetSize * 0.5 colorFg: AudioService.muted ? Color.mError : Color.mOnSurface colorBg: "transparent" colorBgHover: Color.mHover colorFgHover: Color.mOnHover onClicked: AudioService.setOutputMuted(!AudioService.muted) } NText { text: AudioService.sink ? AudioService.sink.description : "No output device" pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant elide: Text.ElideRight Layout.fillWidth: true Layout.preferredWidth: 0 } } // Output Volume Slider NSlider { id: outputVolumeSlider Layout.fillWidth: true from: 0 to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 value: localOutputVolume stepSize: 0.01 heightRatio: 0.5 onMoved: localOutputVolume = value onPressedChanged: localOutputVolumeChanging = pressed tooltipText: `${Math.round((outputVolumeGuard ? localOutputVolume : AudioService.volume) * 100)}%` tooltipDirection: "bottom" // MouseArea to handle wheel events when hovering over the slider MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton propagateComposedEvents: true onWheel: wheel => { if (outputVolumeSlider.enabled && AudioService.sink) { const delta = wheel.angleDelta.y || wheel.angleDelta.x; const step = Settings.data.audio.volumeStep / 100.0; // Convert percentage to 0-1 range const increment = delta > 0 ? step : -step; const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; const newValue = Math.max(0, Math.min(maxVolume, localOutputVolume + increment)); localOutputVolume = newValue; } } } } } // Input Volume Section ColumnLayout { spacing: Style.marginXXS Layout.fillWidth: true Layout.preferredWidth: 0 opacity: AudioService.source ? 1.0 : 0.5 enabled: AudioService.source // Input Volume Header RowLayout { Layout.fillWidth: true spacing: Style.marginXS NIconButton { icon: AudioService.inputMuted ? "microphone-off" : "microphone" baseSize: Style.baseWidgetSize * 0.5 colorFg: AudioService.inputMuted ? Color.mError : Color.mOnSurface colorBg: "transparent" colorBgHover: Color.mHover colorFgHover: Color.mOnHover onClicked: AudioService.setInputMuted(!AudioService.inputMuted) } NText { text: AudioService.source ? AudioService.source.description : "No input device" pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant elide: Text.ElideRight Layout.fillWidth: true Layout.preferredWidth: 0 } } // Input Volume Slider NSlider { id: inputVolumeSlider Layout.fillWidth: true from: 0 to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 value: localInputVolume stepSize: 0.01 heightRatio: 0.5 onMoved: localInputVolume = value onPressedChanged: localInputVolumeChanging = pressed tooltipText: `${Math.round((inputVolumeGuard ? localInputVolume : AudioService.inputVolume) * 100)}%` tooltipDirection: "bottom" // MouseArea to handle wheel events when hovering over the slider MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton propagateComposedEvents: true onWheel: wheel => { if (inputVolumeSlider.enabled && AudioService.source) { const delta = wheel.angleDelta.y || wheel.angleDelta.x; const step = Settings.data.audio.volumeStep / 100.0; // Convert percentage to 0-1 range const increment = delta > 0 ? step : -step; const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; const newValue = Math.max(0, Math.min(maxVolume, localInputVolume + increment)); localInputVolume = newValue; } } } } } } }