summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml
diff options
context:
space:
mode:
authorNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
committerNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
commit3462bcc46ecf74f576ea4397f16492c16bd75b10 (patch)
treec5ab7aef4498647e057bda8d49c11e5f9dda74c9 /raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml
parent56616f693b4f263cbf0d47da43b67424efa6022d (diff)
downloadRaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.tar.gz
RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.zip
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml1075
1 files changed, 1075 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml b/raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml
new file mode 100644
index 0000000..015fedc
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Services/AudioService.qml
@@ -0,0 +1,1075 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtCore
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Pipewire
+import qs.Common
+import qs.Services
+
+Singleton {
+ id: root
+
+ readonly property PwNode sink: Pipewire.defaultAudioSink
+ readonly property PwNode source: Pipewire.defaultAudioSource
+
+ readonly property bool soundsAvailable: MultimediaService.available
+ property bool gsettingsAvailable: false
+ property var availableSoundThemes: []
+ property string currentSoundTheme: ""
+ property var soundFilePaths: ({})
+
+ property var volumeChangeSound: null
+ property var powerPlugSound: null
+ property var powerUnplugSound: null
+ property var normalNotificationSound: null
+ property var criticalNotificationSound: null
+ property var loginSound: null
+ property real notificationsVolume: 1.0
+ property bool notificationsAudioMuted: false
+
+ property var mediaDevices: null
+ property var mediaDevicesConnections: null
+
+ property var deviceAliases: ({})
+ property string wireplumberConfigPath: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf"
+ property bool wireplumberReloading: false
+
+ readonly property int sinkMaxVolume: {
+ const name = sink?.name ?? "";
+ if (!name)
+ return 100;
+ return SessionData.deviceMaxVolumes[name] ?? 100;
+ }
+
+ signal micMuteChanged
+ signal audioOutputCycled(string deviceName, string deviceIcon)
+ signal deviceAliasChanged(string nodeName, string newAlias)
+ signal wireplumberReloadStarted
+ signal wireplumberReloadCompleted(bool success)
+
+ function getMaxVolumePercent(node) {
+ if (!node?.name)
+ return 100;
+ return SessionData.deviceMaxVolumes[node.name] ?? 100;
+ }
+
+ Connections {
+ target: SessionData
+ function onDeviceMaxVolumesChanged() {
+ if (!root.sink?.audio)
+ return;
+ const maxVol = root.sinkMaxVolume;
+ const currentPercent = Math.round(root.sink.audio.volume * 100);
+ if (currentPercent > maxVol)
+ root.sink.audio.volume = maxVol / 100;
+ }
+ }
+
+ // Used in playLoginSoundIfApplicable()
+ Process {
+ id: loginSoundChecker
+ 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));
+ }
+
+ function cycleAudioOutput() {
+ const sinks = getAvailableSinks();
+ if (sinks.length < 2)
+ return null;
+
+ const currentName = root.sink?.name ?? "";
+ const currentIndex = sinks.findIndex(s => s.name === currentName);
+ const nextIndex = (currentIndex + 1) % sinks.length;
+ const nextSink = sinks[nextIndex];
+ Pipewire.preferredDefaultAudioSink = nextSink;
+ const name = displayName(nextSink);
+ audioOutputCycled(name, sinkIcon(nextSink));
+ return name;
+ }
+
+ function getDeviceAlias(nodeName) {
+ if (!nodeName)
+ return null;
+ return deviceAliases[nodeName] || null;
+ }
+
+ function hasDeviceAlias(nodeName) {
+ if (!nodeName)
+ return false;
+ return deviceAliases.hasOwnProperty(nodeName) && deviceAliases[nodeName] !== null && deviceAliases[nodeName] !== "";
+ }
+
+ function setDeviceAlias(nodeName, customAlias) {
+ if (!nodeName) {
+ console.error("AudioService: Cannot set alias - nodeName is empty");
+ return false;
+ }
+
+ if (!customAlias || customAlias.trim() === "") {
+ return removeDeviceAlias(nodeName);
+ }
+
+ const trimmedAlias = customAlias.trim();
+
+ const updated = Object.assign({}, deviceAliases);
+ updated[nodeName] = trimmedAlias;
+ deviceAliases = updated;
+
+ writeWireplumberConfig();
+ deviceAliasChanged(nodeName, trimmedAlias);
+ return true;
+ }
+
+ function removeDeviceAlias(nodeName) {
+ if (!nodeName)
+ return false;
+
+ if (!hasDeviceAlias(nodeName))
+ return false;
+
+ const updated = Object.assign({}, deviceAliases);
+ delete updated[nodeName];
+ deviceAliases = updated;
+
+ writeWireplumberConfig();
+ deviceAliasChanged(nodeName, "");
+ return true;
+ }
+
+ function writeWireplumberConfig() {
+ const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/wireplumber/wireplumber.conf.d";
+ const configContent = generateWireplumberConfig();
+
+ const shellCmd = `mkdir -p "${configDir}" && cat > "${wireplumberConfigPath}" << 'EOFCONFIG'
+${configContent}
+EOFCONFIG
+`;
+
+ Proc.runCommand("writeWireplumberConfig", ["sh", "-c", shellCmd], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.error("AudioService: Failed to write WirePlumber config. Exit code:", exitCode);
+ console.error("AudioService: Error output:", output);
+ ToastService.showError(I18n.tr("Failed to save audio config"), output || "");
+ return;
+ }
+
+ reloadWireplumberConfig();
+ }, 0);
+ }
+
+ function generateWireplumberConfig() {
+ let config = "# Generated by DankMaterialShell - Audio Device Aliases\n";
+ config += "# Do not edit manually - changes will be overwritten\n";
+ config += "# Last updated: " + new Date().toISOString() + "\n\n";
+
+ const aliasKeys = Object.keys(deviceAliases);
+ if (aliasKeys.length === 0) {
+ config += "# No device aliases configured\n";
+ return config;
+ }
+
+ const alsaAliases = [];
+ const bluezAliases = [];
+ const otherAliases = [];
+
+ for (const nodeName of aliasKeys) {
+ const alias = deviceAliases[nodeName];
+ if (!alias)
+ continue;
+
+ const rule = {
+ nodeName: nodeName,
+ alias: alias
+ };
+
+ if (nodeName.includes("alsa")) {
+ alsaAliases.push(rule);
+ } else if (nodeName.includes("bluez")) {
+ bluezAliases.push(rule);
+ } else {
+ otherAliases.push(rule);
+ }
+ }
+
+ if (alsaAliases.length > 0) {
+ config += "monitor.alsa.rules = [\n";
+ for (let i = 0; i < alsaAliases.length; i++) {
+ const rule = alsaAliases[i];
+ config += " {\n";
+ config += ` matches = [ { "node.name" = "${rule.nodeName}" } ]\n`;
+ config += ` actions = { update-props = { "node.description" = "${rule.alias}" } }\n`;
+ config += " }";
+ if (i < alsaAliases.length - 1)
+ config += ",";
+ config += "\n";
+ }
+ config += "]\n\n";
+ }
+
+ if (bluezAliases.length > 0) {
+ config += "monitor.bluez.rules = [\n";
+ for (let i = 0; i < bluezAliases.length; i++) {
+ const rule = bluezAliases[i];
+ config += " {\n";
+ config += ` matches = [ { "node.name" = "${rule.nodeName}" } ]\n`;
+ config += ` actions = { update-props = { "node.description" = "${rule.alias}" } }\n`;
+ config += " }";
+ if (i < bluezAliases.length - 1)
+ config += ",";
+ config += "\n";
+ }
+ config += "]\n\n";
+ }
+
+ if (otherAliases.length > 0) {
+ config += "# Other device aliases (RAOP, USB, and other devices)\n";
+ config += "wireplumber.rules = [\n";
+ for (let i = 0; i < otherAliases.length; i++) {
+ const rule = otherAliases[i];
+ config += " {\n";
+ config += ` matches = [\n`;
+ config += ` { "node.name" = "${rule.nodeName}" }\n`;
+ config += ` ]\n`;
+ config += ` actions = {\n`;
+ config += ` update-props = {\n`;
+ config += ` "node.description" = "${rule.alias}"\n`;
+ config += ` "node.nick" = "${rule.alias}"\n`;
+ config += ` "device.description" = "${rule.alias}"\n`;
+ config += ` }\n`;
+ config += ` }\n`;
+ config += " }";
+ if (i < otherAliases.length - 1)
+ config += ",";
+ config += "\n";
+ }
+ config += "]\n";
+ }
+
+ return config;
+ }
+
+ function reloadWireplumberConfig() {
+ if (wireplumberReloading) {
+ return;
+ }
+
+ wireplumberReloading = true;
+ wireplumberReloadStarted();
+
+ Proc.runCommand("restartWireplumber", ["systemctl", "--user", "restart", "wireplumber"], (output, exitCode) => {
+ wireplumberReloading = false;
+
+ if (exitCode === 0) {
+ ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated"));
+ wireplumberReloadCompleted(true);
+ } else {
+ console.error("AudioService: Failed to restart WirePlumber:", output);
+ ToastService.showError(I18n.tr("Failed to restart audio system"), output);
+ wireplumberReloadCompleted(false);
+ }
+ }, 5000);
+ }
+
+ function loadDeviceAliases() {
+ const configPath = wireplumberConfigPath;
+
+ Proc.runCommand("readWireplumberConfig", ["cat", configPath], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.log("AudioService: No existing WirePlumber config found");
+ return;
+ }
+
+ const aliases = {};
+ const lines = output.split('\n');
+ let currentNodeName = null;
+
+ for (const line of lines) {
+ const nodeNameMatch = line.match(/"node\.name"\s*=\s*"([^"]+)"/);
+ if (nodeNameMatch) {
+ currentNodeName = nodeNameMatch[1];
+ }
+
+ const descriptionMatch = line.match(/"node\.description"\s*=\s*"([^"]+)"/);
+ if (descriptionMatch && currentNodeName) {
+ aliases[currentNodeName] = descriptionMatch[1];
+ currentNodeName = null;
+ }
+ }
+
+ if (Object.keys(aliases).length > 0) {
+ deviceAliases = aliases;
+ console.log("AudioService: Loaded", Object.keys(aliases).length, "device aliases");
+ }
+ }, 0);
+ }
+
+ Connections {
+ target: root.sink?.audio ?? null
+
+ function onVolumeChanged() {
+ if (SessionData.suppressOSD)
+ return;
+ root.playVolumeChangeSoundIfEnabled();
+ }
+ }
+
+ function checkGsettings() {
+ Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
+ gsettingsAvailable = (exitCode === 0);
+ if (gsettingsAvailable) {
+ scanSoundThemes();
+ getCurrentSoundTheme();
+ }
+ }, 0);
+ }
+
+ function scanSoundThemes() {
+ const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS");
+ const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))) : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))];
+
+ const basePaths = searchPaths.map(p => p + "/sounds").join(" ");
+ const script = `
+ for base_dir in ${basePaths}; do
+ [ -d "$base_dir" ] || continue
+ for theme_dir in "$base_dir"/*; do
+ [ -d "$theme_dir/stereo" ] || continue
+ basename "$theme_dir"
+ done
+ done | sort -u
+ `;
+
+ Proc.runCommand("scanSoundThemes", ["sh", "-c", script], (output, exitCode) => {
+ if (exitCode === 0 && output.trim()) {
+ const themes = output.trim().split('\n').filter(t => t && t.length > 0);
+ availableSoundThemes = themes;
+ } else {
+ availableSoundThemes = [];
+ }
+ }, 0);
+ }
+
+ function getCurrentSoundTheme() {
+ Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => {
+ if (exitCode === 0 && output.trim()) {
+ currentSoundTheme = output.trim();
+ console.log("AudioService: Current system sound theme:", currentSoundTheme);
+ if (SettingsData.useSystemSoundTheme) {
+ discoverSoundFiles(currentSoundTheme);
+ }
+ } else {
+ currentSoundTheme = "";
+ console.log("AudioService: No system sound theme found");
+ }
+ }, 0);
+ }
+
+ function setSoundTheme(themeName) {
+ if (!themeName || themeName === currentSoundTheme) {
+ return;
+ }
+
+ Proc.runCommand("setSoundTheme", ["sh", "-c", `gsettings set org.gnome.desktop.sound theme-name '${themeName}'`], (output, exitCode) => {
+ if (exitCode === 0) {
+ currentSoundTheme = themeName;
+ if (SettingsData.useSystemSoundTheme) {
+ discoverSoundFiles(themeName);
+ }
+ }
+ }, 0);
+ }
+
+ function discoverSoundFiles(themeName) {
+ if (!themeName) {
+ soundFilePaths = {};
+ if (soundsAvailable) {
+ destroySoundPlayers();
+ createSoundPlayers();
+ }
+ return;
+ }
+
+ const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS");
+ const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))) : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))];
+
+ const extensions = ["oga", "ogg", "wav", "mp3", "flac"];
+ const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
+
+ const script = `
+ for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
+ found=0
+
+ case "$event_key" in
+ message)
+ names="dialog-information message message-lowpriority bell"
+ ;;
+ message-new-instant)
+ names="dialog-warning message-new-instant message-highlight"
+ ;;
+ *)
+ names="$event_key"
+ ;;
+ esac
+
+ for theme in ${themesToSearch}; do
+ for event_name in $names; do
+ for base_path in ${searchPaths.join(" ")}; do
+ sounds_path="$base_path/sounds"
+ for ext in ${extensions.join(" ")}; do
+ file_path="$sounds_path/$theme/stereo/$event_name.$ext"
+ if [ -f "$file_path" ]; then
+ echo "$event_key=$file_path"
+ found=1
+ break
+ fi
+ done
+ [ $found -eq 1 ] && break
+ done
+ [ $found -eq 1 ] && break
+ done
+ [ $found -eq 1 ] && break
+ done
+ done
+ `;
+
+ Proc.runCommand("discoverSoundFiles", ["sh", "-c", script], (output, exitCode) => {
+ const paths = {};
+ if (exitCode === 0 && output.trim()) {
+ const lines = output.trim().split('\n');
+ for (let line of lines) {
+ const parts = line.split('=');
+ if (parts.length === 2) {
+ paths[parts[0]] = "file://" + parts[1];
+ }
+ }
+ }
+ soundFilePaths = paths;
+
+ if (soundsAvailable) {
+ destroySoundPlayers();
+ createSoundPlayers();
+ }
+ }, 0);
+ }
+
+ function getSoundPath(soundEvent) {
+ const soundMap = {
+ "audio-volume-change": "../assets/sounds/freedesktop/audio-volume-change.wav",
+ "power-plug": "../assets/sounds/plasma/power-plug.wav",
+ "power-unplug": "../assets/sounds/plasma/power-unplug.wav",
+ "message": "../assets/sounds/freedesktop/message.wav",
+ "message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
+ "desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
+ };
+
+ const specialConditions = {
+ "smooth": ["audio-volume-change"]
+ };
+
+ const themeLower = currentSoundTheme.toLowerCase();
+ if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) {
+ const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav");
+ console.log("AudioService: Using bundled sound (special condition) for", soundEvent, ":", bundledPath);
+ return bundledPath;
+ }
+
+ if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) {
+ console.log("AudioService: Using system sound for", soundEvent, ":", soundFilePaths[soundEvent]);
+ return soundFilePaths[soundEvent];
+ }
+
+ const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav");
+ console.log("AudioService: Using bundled sound for", soundEvent, ":", bundledPath);
+ return bundledPath;
+ }
+
+ function reloadSounds() {
+ console.log("AudioService: Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme);
+ if (SettingsData.useSystemSoundTheme && currentSoundTheme) {
+ discoverSoundFiles(currentSoundTheme);
+ } else {
+ soundFilePaths = {};
+ if (soundsAvailable) {
+ destroySoundPlayers();
+ createSoundPlayers();
+ }
+ }
+ }
+
+ function setupMediaDevices() {
+ if (!soundsAvailable || mediaDevices) {
+ return;
+ }
+
+ try {
+ mediaDevices = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaDevices {
+ id: devices
+ Component.onCompleted: {
+ console.log("AudioService: MediaDevices initialized, default output:", defaultAudioOutput?.description)
+ }
+ }
+ `, root, "AudioService.MediaDevices");
+
+ if (mediaDevices) {
+ mediaDevicesConnections = Qt.createQmlObject(`
+ import QtQuick
+ Connections {
+ target: root.mediaDevices
+ function onDefaultAudioOutputChanged() {
+ console.log("AudioService: Default audio output changed, recreating sound players")
+ root.destroySoundPlayers()
+ root.createSoundPlayers()
+ }
+ }
+ `, root, "AudioService.MediaDevicesConnections");
+ }
+ } catch (e) {
+ console.log("AudioService: MediaDevices not available, using default audio output");
+ mediaDevices = null;
+ }
+ }
+
+ function destroySoundPlayers() {
+ if (volumeChangeSound) {
+ volumeChangeSound.destroy();
+ volumeChangeSound = null;
+ }
+ if (powerPlugSound) {
+ powerPlugSound.destroy();
+ powerPlugSound = null;
+ }
+ if (powerUnplugSound) {
+ powerUnplugSound.destroy();
+ powerUnplugSound = null;
+ }
+ if (normalNotificationSound) {
+ normalNotificationSound.destroy();
+ normalNotificationSound = null;
+ }
+ if (criticalNotificationSound) {
+ criticalNotificationSound.destroy();
+ criticalNotificationSound = null;
+ }
+ if (loginSound) {
+ loginSound.destroy();
+ loginSound = null;
+ }
+ }
+
+ function createSoundPlayers() {
+ if (!soundsAvailable) {
+ return;
+ }
+
+ setupMediaDevices();
+
+ try {
+ const deviceProperty = mediaDevices ? `device: root.mediaDevices.defaultAudioOutput\n ` : "";
+
+ const volumeChangePath = getSoundPath("audio-volume-change");
+ volumeChangeSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${volumeChangePath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.VolumeChangeSound");
+
+ const powerPlugPath = getSoundPath("power-plug");
+ powerPlugSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${powerPlugPath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.PowerPlugSound");
+
+ const powerUnplugPath = getSoundPath("power-unplug");
+ powerUnplugSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${powerUnplugPath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.PowerUnplugSound");
+
+ const messagePath = getSoundPath("message");
+ normalNotificationSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${messagePath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.NormalNotificationSound");
+
+ const messageNewInstantPath = getSoundPath("message-new-instant");
+ criticalNotificationSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${messageNewInstantPath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.CriticalNotificationSound");
+
+ const loginPath = getSoundPath("desktop-login");
+ loginSound = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ MediaPlayer {
+ source: "${loginPath}"
+ audioOutput: AudioOutput {
+ ${deviceProperty}volume: notificationsVolume
+ }
+ }
+ `, root, "AudioService.LoginSound");
+
+ } catch (e) {
+ console.warn("AudioService: Error creating sound players:", e);
+ }
+ }
+
+ function isMediaPlaying() {
+ return MprisController.activePlayer?.isPlaying ?? false;
+ }
+
+ function playVolumeChangeSound() {
+ if (!soundsAvailable || !volumeChangeSound || notificationsAudioMuted || isMediaPlaying())
+ return;
+ volumeChangeSound.play();
+ }
+
+ function playPowerPlugSound() {
+ if (!soundsAvailable || !powerPlugSound || notificationsAudioMuted || isMediaPlaying())
+ return;
+ powerPlugSound.play();
+ }
+
+ function playPowerUnplugSound() {
+ if (!soundsAvailable || !powerUnplugSound || notificationsAudioMuted || isMediaPlaying())
+ return;
+ powerUnplugSound.play();
+ }
+
+ function playNormalNotificationSound() {
+ if (!soundsAvailable || !normalNotificationSound || SessionData.doNotDisturb || notificationsAudioMuted || isMediaPlaying())
+ return;
+ normalNotificationSound.play();
+ }
+
+ function playCriticalNotificationSound() {
+ if (!soundsAvailable || !criticalNotificationSound || SessionData.doNotDisturb || notificationsAudioMuted || isMediaPlaying())
+ return;
+ criticalNotificationSound.play();
+ }
+
+ function playLoginSound() {
+ if (!soundsAvailable || !loginSound || notificationsAudioMuted || isMediaPlaying()) {
+ return;
+ }
+ loginSound.play();
+ }
+
+ function playLoginSoundIfApplicable() {
+ if (SettingsData.soundsEnabled && SettingsData.soundLogin && !notificationsAudioMuted) {
+ // plays login sound on session start, but only if a specific file doesn't exist,
+ // to prevent it from playing on every DMS restart during the session
+ const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
+ const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
+
+ if (!runtimeDir) return;
+
+ const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;
+
+ // if file doesn't exist, touch it (0)
+ // If it exists, do nothing (1)
+ loginSoundChecker.command = ["sh", "-c", `[ ! -f ${loginFile} ] && touch ${loginFile}`];
+ loginSoundChecker.running = true;
+ }
+ }
+
+ function playVolumeChangeSoundIfEnabled() {
+ if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
+ playVolumeChangeSound();
+ }
+ }
+
+ function sinkIcon(node) {
+ if (!node)
+ return "speaker";
+
+ const props = node.properties || {};
+ const formFactor = (props["device.form-factor"] || "").toLowerCase();
+
+ switch (formFactor) {
+ case "headphone":
+ case "headset":
+ case "hands-free":
+ case "handset":
+ return "headset";
+ case "tv":
+ case "monitor":
+ return "tv";
+ case "speaker":
+ case "computer":
+ case "hifi":
+ case "portable":
+ case "car":
+ return "speaker";
+ }
+
+ const bus = (props["device.bus"] || "").toLowerCase();
+ if (bus === "bluetooth")
+ return "headset";
+
+ const name = (node.name || "").toLowerCase();
+ if (name.includes("hdmi"))
+ return "tv";
+ if (name.includes("iec958") || name.includes("spdif"))
+ return "speaker";
+
+ if (bus === "usb")
+ return "headset";
+
+ return "speaker";
+ }
+
+ function displayName(node) {
+ if (!node) {
+ return "";
+ }
+
+ // FIRST: Check if we have a custom alias in our deviceAliases map
+ // This ensures we always show the user's custom name, regardless of
+ // whether WirePlumber has applied it to the node properties yet
+ if (node.name && deviceAliases[node.name]) {
+ return deviceAliases[node.name];
+ }
+
+ // Check node.properties["node.description"] for WirePlumber-applied aliases
+ // This is the live property updated by WirePlumber rules
+ if (node.properties && node.properties["node.description"]) {
+ const desc = node.properties["node.description"];
+ if (desc !== node.name) {
+ return desc;
+ }
+ }
+
+ // Check cached description as fallback
+ if (node.description && node.description !== node.name) {
+ return node.description;
+ }
+
+ // Fallback to device description property
+ if (node.properties && node.properties["device.description"]) {
+ return node.properties["device.description"];
+ }
+
+ // Fallback to nickname
+ if (node.nickname && node.nickname !== node.name) {
+ return node.nickname;
+ }
+
+ // Fallback to friendly names based on node name patterns
+ if (node.name.includes("analog-stereo")) {
+ return "Built-in Audio Analog Stereo";
+ }
+ if (node.name.includes("bluez")) {
+ return "Bluetooth Audio";
+ }
+ if (node.name.includes("usb")) {
+ return "USB Audio";
+ }
+ if (node.name.includes("hdmi")) {
+ return "HDMI Audio";
+ }
+
+ return node.name;
+ }
+
+ function originalName(node) {
+ if (!node) {
+ return "";
+ }
+
+ // Get the original name without checking for custom aliases
+ // Check pattern-based friendly names FIRST (before device.description)
+ // This ensures we show user-friendly names like "Built-in Audio Analog Stereo"
+ // instead of hardware chip names like "ALC274 Analog"
+ if (node.name.includes("analog-stereo")) {
+ return "Built-in Audio Analog Stereo";
+ }
+ if (node.name.includes("bluez")) {
+ return "Bluetooth Audio";
+ }
+ if (node.name.includes("usb")) {
+ return "USB Audio";
+ }
+ if (node.name.includes("hdmi")) {
+ return "HDMI Audio";
+ }
+ if (node.name.includes("raop_sink")) {
+ // Extract friendly name from RAOP node name
+ const match = node.name.match(/raop_sink\.([^.]+)/);
+ if (match) {
+ return match[1].replace(/-/g, " ");
+ }
+ }
+
+ // Fallback to device.description property
+ if (node.properties && node.properties["device.description"]) {
+ return node.properties["device.description"];
+ }
+
+ // Fallback to nickname
+ if (node.nickname && node.nickname !== node.name) {
+ return node.nickname;
+ }
+
+ return node.name;
+ }
+
+ function subtitle(name) {
+ if (!name) {
+ return "";
+ }
+
+ if (name.includes('usb-')) {
+ if (name.includes('SteelSeries')) {
+ return "USB Gaming Headset";
+ }
+ if (name.includes('Generic')) {
+ return "USB Audio Device";
+ }
+ return "USB Audio";
+ }
+
+ if (name.includes('pci-')) {
+ if (name.includes('01_00.1') || name.includes('01:00.1')) {
+ return "NVIDIA GPU Audio";
+ }
+ return "PCI Audio";
+ }
+
+ if (name.includes('bluez')) {
+ return "Bluetooth Audio";
+ }
+ if (name.includes('analog')) {
+ return "Built-in Audio";
+ }
+ if (name.includes('hdmi')) {
+ return "HDMI Audio";
+ }
+
+ return "";
+ }
+
+ PwObjectTracker {
+ objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
+ }
+
+ Connections {
+ target: Pipewire
+ function onDefaultAudioSinkChanged() {
+ if (soundsAvailable) {
+ Qt.callLater(root.destroySoundPlayers);
+ Qt.callLater(root.createSoundPlayers);
+ }
+ }
+ }
+
+ function setVolume(percentage) {
+ if (!root.sink?.audio)
+ return "No audio sink available";
+
+ const maxVol = root.sinkMaxVolume;
+ const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
+ root.sink.audio.volume = clampedVolume / 100;
+ return `Volume set to ${clampedVolume}%`;
+ }
+
+ function toggleMute() {
+ if (!root.sink?.audio) {
+ return "No audio sink available";
+ }
+
+ root.sink.audio.muted = !root.sink.audio.muted;
+ return root.sink.audio.muted ? "Audio muted" : "Audio unmuted";
+ }
+
+ function setMicVolume(percentage) {
+ if (!root.source?.audio) {
+ return "No audio source available";
+ }
+
+ const clampedVolume = Math.max(0, Math.min(100, percentage));
+ root.source.audio.volume = clampedVolume / 100;
+ return `Microphone volume set to ${clampedVolume}%`;
+ }
+
+ function toggleMicMute() {
+ if (!root.source?.audio) {
+ return "No audio source available";
+ }
+
+ root.source.audio.muted = !root.source.audio.muted;
+ return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted";
+ }
+
+ IpcHandler {
+ target: "audio"
+
+ function setvolume(percentage: string): string {
+ return root.setVolume(parseInt(percentage));
+ }
+
+ function increment(step: string): string {
+ if (!root.sink?.audio)
+ return "No audio sink available";
+
+ if (root.sink.audio.muted)
+ root.sink.audio.muted = false;
+
+ const maxVol = root.sinkMaxVolume;
+ const currentVolume = Math.round(root.sink.audio.volume * 100);
+ const stepValue = parseInt(step || "5");
+ const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue));
+
+ root.sink.audio.volume = newVolume / 100;
+ return `Volume increased to ${newVolume}%`;
+ }
+
+ function decrement(step: string): string {
+ if (!root.sink?.audio)
+ return "No audio sink available";
+
+ if (root.sink.audio.muted)
+ root.sink.audio.muted = false;
+
+ const maxVol = root.sinkMaxVolume;
+ const currentVolume = Math.round(root.sink.audio.volume * 100);
+ const stepValue = parseInt(step || "5");
+ const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue));
+
+ root.sink.audio.volume = newVolume / 100;
+ return `Volume decreased to ${newVolume}%`;
+ }
+
+ function mute(): string {
+ return root.toggleMute();
+ }
+
+ function setmic(percentage: string): string {
+ return root.setMicVolume(parseInt(percentage));
+ }
+
+ function micmute(): string {
+ const result = root.toggleMicMute();
+ root.micMuteChanged();
+ return result;
+ }
+
+ function status(): string {
+ let result = "Audio Status:\n";
+
+ if (root.sink?.audio) {
+ const volume = Math.round(root.sink.audio.volume * 100);
+ const muteStatus = root.sink.audio.muted ? " (muted)" : "";
+ const maxVol = root.sinkMaxVolume;
+ result += `Output: ${volume}%${muteStatus} (max: ${maxVol}%)\n`;
+ } else {
+ result += "Output: No sink available\n";
+ }
+
+ if (root.source?.audio) {
+ const micVolume = Math.round(root.source.audio.volume * 100);
+ const muteStatus = root.source.audio.muted ? " (muted)" : "";
+ result += `Input: ${micVolume}%${muteStatus}`;
+ } else {
+ result += "Input: No source available";
+ }
+
+ return result;
+ }
+
+ function getmaxvolume(): string {
+ return `${root.sinkMaxVolume}`;
+ }
+
+ function setmaxvolume(percent: string): string {
+ if (!root.sink?.name)
+ return "No audio sink available";
+ const val = parseInt(percent);
+ if (isNaN(val))
+ return "Invalid percentage";
+ SessionData.setDeviceMaxVolume(root.sink.name, val);
+ return `Max volume set to ${SessionData.getDeviceMaxVolume(root.sink.name)}%`;
+ }
+
+ function getmaxvolumefor(nodeName: string): string {
+ if (!nodeName)
+ return "No node name specified";
+ return `${SessionData.getDeviceMaxVolume(nodeName)}`;
+ }
+
+ function setmaxvolumefor(nodeName: string, percent: string): string {
+ if (!nodeName)
+ return "No node name specified";
+ const val = parseInt(percent);
+ if (isNaN(val))
+ return "Invalid percentage";
+ SessionData.setDeviceMaxVolume(nodeName, val);
+ return `Max volume for ${nodeName} set to ${SessionData.getDeviceMaxVolume(nodeName)}%`;
+ }
+
+ function cycleoutput(): string {
+ const result = root.cycleAudioOutput();
+ if (!result)
+ return "Only one audio output available";
+ return `Switched to: ${result}`;
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onUseSystemSoundThemeChanged() {
+ reloadSounds();
+ }
+ }
+
+ Component.onCompleted: {
+ if (soundsAvailable) {
+ checkGsettings();
+ Qt.callLater(createSoundPlayers);
+ }
+
+ loadDeviceAliases();
+ }
+}