diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index 4cdd9797..ff70ed6f 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -260,7 +260,7 @@ Column { } case "audioOutput": { - if (!AudioService.sink) + if (!AudioService.sink?.audio) return "volume_off"; let volume = AudioService.sink.audio.volume; let muted = AudioService.sink.audio.muted; @@ -276,7 +276,7 @@ Column { } case "audioInput": { - if (!AudioService.source) + if (!AudioService.source?.audio) return "mic_off"; let muted = AudioService.source.audio.muted; return muted ? "mic_off" : "mic"; @@ -369,7 +369,7 @@ Column { } case "audioOutput": { - if (!AudioService.sink) + if (!AudioService.sink?.audio) return I18n.tr("Select device", "audio status"); if (AudioService.sink.audio.muted) return I18n.tr("Muted", "audio status"); @@ -380,7 +380,7 @@ Column { } case "audioInput": { - if (!AudioService.source) + if (!AudioService.source?.audio) return I18n.tr("Select device", "audio status"); if (AudioService.source.audio.muted) return I18n.tr("Muted", "audio status"); @@ -412,9 +412,9 @@ Column { case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled); case "audioOutput": - return !!(AudioService.sink && !AudioService.sink.audio.muted); + return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted); case "audioInput": - return !!(AudioService.source && !AudioService.source.audio.muted); + return !!(AudioService.source?.audio && !AudioService.source.audio.muted); default: return false; } diff --git a/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml b/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml index 8fc2d46c..25f3a9e3 100644 --- a/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml @@ -351,8 +351,8 @@ Rectangle { deviceRipple.trigger(mapped.x, mapped.y); } onClicked: { - if (modelData) { - Pipewire.preferredDefaultAudioSource = modelData; + if (modelData && modelData.name) { + AudioService.setDefaultSourceByName(modelData.name); } } } diff --git a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml index 7e86ae47..95d862d8 100644 --- a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml @@ -355,8 +355,8 @@ Rectangle { deviceRipple.trigger(mapped.x, mapped.y); } onClicked: { - if (modelData) { - Pipewire.preferredDefaultAudioSink = modelData; + if (modelData && modelData.name) { + AudioService.setDefaultSinkByName(modelData.name); } } } diff --git a/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml index 81d346d6..c53c06b6 100644 --- a/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml @@ -35,7 +35,7 @@ Row { cursorShape: Qt.PointingHandCursor onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y) onClicked: { - if (defaultSink) { + if (defaultSink?.audio) { SessionData.suppressOSDTemporarily(); defaultSink.audio.muted = !defaultSink.audio.muted; } @@ -45,7 +45,7 @@ Row { DankIcon { anchors.centerIn: parent name: { - if (!defaultSink) + if (!defaultSink?.audio) return "volume_off"; let volume = defaultSink.audio.volume; @@ -62,18 +62,18 @@ Row { return "volume_up"; } size: Theme.iconSize - color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText + color: defaultSink?.audio && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText } } DankSlider { id: volumeSlider - readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0 + readonly property real actualVolumePercent: defaultSink?.audio ? Math.round(defaultSink.audio.volume * 100) : 0 anchors.verticalCenter: parent.verticalCenter width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - enabled: defaultSink !== null + enabled: defaultSink?.audio != null minimum: 0 maximum: AudioService.sinkMaxVolume showValue: true @@ -83,7 +83,7 @@ Row { trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) onSliderValueChanged: function (newValue) { - if (defaultSink) { + if (defaultSink?.audio) { SessionData.suppressOSDTemporarily(); defaultSink.audio.volume = newValue / 100.0; if (newValue > 0 && defaultSink.audio.muted) { @@ -97,7 +97,7 @@ Row { Binding { target: volumeSlider property: "value" - value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0 + value: defaultSink?.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0 when: !volumeSlider.isDragging } } diff --git a/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml index 49ea30e7..5c631bcd 100644 --- a/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml @@ -35,7 +35,7 @@ Row { cursorShape: Qt.PointingHandCursor onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y) onClicked: { - if (defaultSource) { + if (defaultSource?.audio) { SessionData.suppressOSDTemporarily(); defaultSource.audio.muted = !defaultSource.audio.muted; } @@ -45,7 +45,7 @@ Row { DankIcon { anchors.centerIn: parent name: { - if (!defaultSource) + if (!defaultSource?.audio) return "mic_off"; let volume = defaultSource.audio.volume; @@ -56,26 +56,26 @@ Row { return "mic"; } size: Theme.iconSize - color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText + color: defaultSource?.audio && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText } } DankSlider { - readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0 + readonly property real actualVolumePercent: defaultSource?.audio ? Math.round(defaultSource.audio.volume * 100) : 0 anchors.verticalCenter: parent.verticalCenter width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - enabled: defaultSource !== null + enabled: defaultSource?.audio != null minimum: 0 maximum: 100 - value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0 + value: defaultSource?.audio ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0 showValue: true unit: "%" valueOverride: actualVolumePercent thumbOutlineColor: Theme.surfaceContainer trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) onSliderValueChanged: function (newValue) { - if (defaultSource) { + if (defaultSource?.audio) { SessionData.suppressOSDTemporarily(); defaultSource.audio.volume = newValue / 100.0; if (newValue > 0 && defaultSource.audio.muted) { diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 35dfe974..d1e94dd8 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import Quickshell.Services.Pipewire import qs.Common import qs.Services @@ -25,7 +24,7 @@ Item { } property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) - property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio) + property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)) property var availableDevices: { const hidden = SessionData.hiddenOutputDeviceNames ?? []; return Pipewire.nodes.values.filter(node => { @@ -336,8 +335,8 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (modelData) { - Pipewire.preferredDefaultAudioSink = modelData; + if (modelData && modelData.name) { + AudioService.setDefaultSinkByName(modelData.name); root.deviceSelected(modelData); } } diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index c6d7bfce..9015052a 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -57,7 +57,7 @@ Item { const id = activePlayer.identity.toLowerCase(); return id.includes("chrome") || id.includes("chromium"); } - readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio) + readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)) readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index 015fedc3..31a17448 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -71,18 +71,48 @@ Singleton { // Used in playLoginSoundIfApplicable() Process { id: loginSoundChecker - onExited: (exitCode) => { + onExited: exitCode => { if (exitCode === 0) { playLoginSound(); } + } } -} function getAvailableSinks() { const hidden = SessionData.hiddenOutputDeviceNames ?? []; return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name)); } + // Resolve a PwNode by name from the live typed list and assign it as the + // default sink. Going through Pipewire.nodes.values directly (no .filter + // / spread / .sort / property var) avoids QML type erasure to QObject*, + // which newer quickshell rejects when assigning to preferredDefaultAudioSink. + function setDefaultSinkByName(name) { + if (!name) + return false; + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i]; + if (node && node.name === name && node.audio && node.isSink && !node.isStream) { + Pipewire.preferredDefaultAudioSink = node; + return true; + } + } + return false; + } + + function setDefaultSourceByName(name) { + if (!name) + return false; + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i]; + if (node && node.name === name && node.audio && !node.isSink && !node.isStream) { + Pipewire.preferredDefaultAudioSource = node; + return true; + } + } + return false; + } + function cycleAudioOutput() { const sinks = getAvailableSinks(); if (sinks.length < 2) @@ -92,7 +122,8 @@ Singleton { const currentIndex = sinks.findIndex(s => s.name === currentName); const nextIndex = (currentIndex + 1) % sinks.length; const nextSink = sinks[nextIndex]; - Pipewire.preferredDefaultAudioSink = nextSink; + if (!setDefaultSinkByName(nextSink.name)) + Pipewire.preferredDefaultAudioSink = nextSink; const name = displayName(nextSink); audioOutputCycled(name, sinkIcon(nextSink)); return name; @@ -650,7 +681,6 @@ EOFCONFIG } } `, root, "AudioService.LoginSound"); - } catch (e) { console.warn("AudioService: Error creating sound players:", e); } @@ -704,7 +734,8 @@ EOFCONFIG const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR"); const sessionId = Quickshell.env("XDG_SESSION_ID") || "0"; - if (!runtimeDir) return; + if (!runtimeDir) + return; const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;