pragma Singleton import qs.config import Caelestia.Services import Caelestia import Quickshell import Quickshell.Services.Pipewire import QtQuick Singleton { id: root property string previousSinkName: "" property string previousSourceName: "" readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { if (!node.isStream) { if (node.isSink) acc.sinks.push(node); else if (node.audio) acc.sources.push(node); } else if (node.isStream && node.audio) { // Application streams (output streams) acc.streams.push(node); } return acc; }, { sources: [], sinks: [], streams: [] }) readonly property list sinks: nodes.sinks readonly property list sources: nodes.sources readonly property list streams: nodes.streams readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource readonly property bool muted: !!sink?.audio?.muted readonly property real volume: sink?.audio?.volume ?? 0 readonly property bool sourceMuted: !!source?.audio?.muted readonly property real sourceVolume: source?.audio?.volume ?? 0 readonly property alias cava: cava readonly property alias beatTracker: beatTracker function setVolume(newVolume: real): void { if (sink?.ready && sink?.audio) { sink.audio.muted = false; sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function incrementVolume(amount: real): void { setVolume(volume + (amount || Config.services.audioIncrement)); } function decrementVolume(amount: real): void { setVolume(volume - (amount || Config.services.audioIncrement)); } function setSourceVolume(newVolume: real): void { if (source?.ready && source?.audio) { source.audio.muted = false; source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function incrementSourceVolume(amount: real): void { setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); } function decrementSourceVolume(amount: real): void { setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); } function setAudioSink(newSink: PwNode): void { Pipewire.preferredDefaultAudioSink = newSink; } function setAudioSource(newSource: PwNode): void { Pipewire.preferredDefaultAudioSource = newSource; } function setStreamVolume(stream: PwNode, newVolume: real): void { if (stream?.ready && stream?.audio) { stream.audio.muted = false; stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function setStreamMuted(stream: PwNode, muted: bool): void { if (stream?.ready && stream?.audio) { stream.audio.muted = muted; } } function getStreamVolume(stream: PwNode): real { return stream?.audio?.volume ?? 0; } function getStreamMuted(stream: PwNode): bool { return !!stream?.audio?.muted; } function getStreamName(stream: PwNode): string { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { if (!sink?.ready) return; const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); previousSinkName = newSinkName; } onSourceChanged: { if (!source?.ready) return; const newSourceName = source.description || source.name || qsTr("Unknown Device"); if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); previousSourceName = newSourceName; } Component.onCompleted: { previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device"); previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); } PwObjectTracker { objects: [...root.sinks, ...root.sources, ...root.streams] } CavaProvider { id: cava bars: Config.services.visualiserBars } BeatTracker { id: beatTracker } }