diff options
| author | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
|---|---|---|
| committer | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
| commit | 5d94c0a7d44a2255b81815a52a7056a94a39842d (patch) | |
| tree | 759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/AudioService.qml | |
| parent | e79cdf210b267f21a186255ce1a4d50029439d54 (diff) | |
| download | RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip | |
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/AudioService.qml')
| -rw-r--r-- | raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/AudioService.qml | 1075 |
1 files changed, 1075 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/AudioService.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/AudioService.qml new file mode 100644 index 0000000..015fedc --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/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(); + } +} |