1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-05 05:12:05 -04:00

feat: Alias for Audio Devices

- New custom audio UI to set custom names for input/output devices
This commit is contained in:
purian23
2026-02-04 07:09:55 -05:00
parent 6e3b3ce888
commit 961680af8c
5 changed files with 983 additions and 8 deletions

View File

@@ -458,5 +458,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: audioLoader
anchors.fill: parent
active: root.currentIndex === 29
visible: active
focus: active
sourceComponent: AudioTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}

View File

@@ -239,12 +239,12 @@ Rectangle {
"icon": "computer",
"collapsedByDefault": true,
"children": [
{
"id": "printers",
"text": I18n.tr("Printers"),
"icon": "print",
"tabIndex": 8,
"cupsOnly": true
"id": "audio",
"text": I18n.tr("Audio"),
"icon": "headphones",
"tabIndex": 29
},
{
"id": "clipboard",
@@ -253,6 +253,13 @@ Rectangle {
"tabIndex": 23,
"clipboardOnly": true
},
{
"id": "printers",
"text": I18n.tr("Printers"),
"icon": "print",
"tabIndex": 8,
"cupsOnly": true
},
{
"id": "window_rules",
"text": I18n.tr("Window Rules"),

View File

@@ -0,0 +1,503 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
property var outputDevices: []
property var inputDevices: []
property bool showEditDialog: false
property var editingDevice: null
property string editingDeviceType: ""
property string newDeviceName: ""
property bool isReloadingAudio: false
function updateDeviceList() {
const allNodes = Pipewire.nodes.values;
// Sort devices: active first, then alphabetically by name
const sortDevices = (a, b) => {
if (a === AudioService.sink && b !== AudioService.sink)
return -1;
if (b === AudioService.sink && a !== AudioService.sink)
return 1;
const nameA = AudioService.displayName(a).toLowerCase();
const nameB = AudioService.displayName(b).toLowerCase();
return nameA.localeCompare(nameB);
};
const outputs = allNodes.filter(node => {
return node.audio && node.isSink && !node.isStream;
});
outputDevices = outputs.sort(sortDevices);
const inputs = allNodes.filter(node => {
return node.audio && !node.isSink && !node.isStream;
});
const sortInputs = (a, b) => {
if (a === AudioService.source && b !== AudioService.source)
return -1;
if (b === AudioService.source && a !== AudioService.source)
return 1;
const nameA = AudioService.displayName(a).toLowerCase();
const nameB = AudioService.displayName(b).toLowerCase();
return nameA.localeCompare(nameB);
};
inputDevices = inputs.sort(sortInputs);
}
Component.onCompleted: {
updateDeviceList();
}
Connections {
target: Pipewire.nodes
function onValuesChanged() {
root.updateDeviceList();
}
}
Connections {
target: AudioService
function onWireplumberReloadStarted() {
root.isReloadingAudio = true;
}
function onWireplumberReloadCompleted(success) {
Qt.callLater(() => {
delayTimer.start();
});
}
function onDeviceAliasChanged(nodeName, newAlias) {
root.updateDeviceList();
}
}
Timer {
id: delayTimer
interval: 2000
repeat: false
onTriggered: {
root.isReloadingAudio = false;
root.updateDeviceList();
}
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "audio"
tags: ["audio", "device", "output", "speaker"]
title: I18n.tr("Output Devices")
settingKey: "audioOutputDevices"
iconName: "volume_up"
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
width: parent.width
text: I18n.tr("Set custom names for your audio output devices")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
Repeater {
model: root.outputDevices
delegate: DeviceAliasRow {
required property var modelData
deviceNode: modelData
deviceType: "output"
onEditRequested: device => {
root.editingDevice = device;
root.editingDeviceType = "output";
root.newDeviceName = AudioService.displayName(device);
root.showEditDialog = true;
}
onResetRequested: device => {
AudioService.removeDeviceAlias(device.name);
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No output devices found")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: root.outputDevices.length === 0
topPadding: Theme.spacingM
}
}
}
SettingsCard {
tab: "audio"
tags: ["audio", "device", "input", "microphone"]
title: I18n.tr("Input Devices")
settingKey: "audioInputDevices"
iconName: "mic"
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
width: parent.width
text: I18n.tr("Set custom names for your audio input devices")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
Repeater {
model: root.inputDevices
delegate: DeviceAliasRow {
required property var modelData
deviceNode: modelData
deviceType: "input"
onEditRequested: device => {
root.editingDevice = device;
root.editingDeviceType = "input";
root.newDeviceName = AudioService.displayName(device);
root.showEditDialog = true;
}
onResetRequested: device => {
AudioService.removeDeviceAlias(device.name);
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No input devices found")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: root.inputDevices.length === 0
topPadding: Theme.spacingM
}
}
}
}
}
Rectangle {
id: loadingOverlay
anchors.fill: parent
color: Theme.withAlpha(Theme.surface, 0.9)
visible: root.isReloadingAudio
z: 100
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
Rectangle {
width: 80
height: 80
radius: 40
color: Theme.primaryContainer
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
id: spinningIcon
name: "refresh"
size: 40
color: Theme.primary
anchors.centerIn: parent
RotationAnimator {
target: spinningIcon
from: 0
to: 360
duration: 1500
loops: Animation.Infinite
running: loadingOverlay.visible
}
}
}
Column {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: I18n.tr("Restarting audio system...")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("This may take a few seconds")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
id: dialogOverlay
anchors.fill: parent
visible: root.showEditDialog
color: Theme.withAlpha(Theme.surface, 0.8)
z: 1000
MouseArea {
anchors.fill: parent
onClicked: {
root.showEditDialog = false;
}
}
Rectangle {
id: editDialog
anchors.centerIn: parent
width: Math.min(500, parent.width - Theme.spacingL * 4)
height: dialogContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineMedium
MouseArea {
anchors.fill: parent
onClicked: {}
}
Column {
id: dialogContent
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.editingDeviceType === "input" ? "mic" : "speaker"
size: Theme.iconSize + 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - 8
spacing: 4
StyledText {
text: I18n.tr("Set Custom Device Name")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
wrapMode: Text.Wrap
}
StyledText {
text: root.editingDevice?.name ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
StyledText {
visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "")
text: I18n.tr("Original: %1").arg(AudioService.originalName(root.editingDevice))
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
opacity: 0.7
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Custom Name")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
DankTextField {
id: nameInput
width: parent.width
placeholderText: I18n.tr("Enter device name...")
text: root.newDeviceName
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
showClearButton: true
onTextChanged: {
root.newDeviceName = text;
}
Keys.onReturnPressed: {
if (text.trim() !== "") {
saveButtonMouseArea.clicked(null);
}
}
Keys.onEscapePressed: {
root.showEditDialog = false;
}
Component.onCompleted: {
Qt.callLater(() => {
forceActiveFocus();
selectAll();
});
}
}
StyledText {
width: parent.width
text: I18n.tr("Press Enter and the audio system will restart to apply the change")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
Row {
width: parent.width
spacing: Theme.spacingM
layoutDirection: Qt.RightToLeft
Rectangle {
id: saveButton
width: saveButtonContent.width + Theme.spacingL * 2
height: Theme.buttonHeight
radius: Theme.cornerRadius
color: saveButtonMouseArea.containsMouse ? Theme.primaryContainer : Theme.primary
enabled: root.newDeviceName.trim() !== ""
opacity: enabled ? 1.0 : 0.5
Row {
id: saveButtonContent
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "check"
size: Theme.iconSize - 4
color: Theme.onPrimary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.onPrimary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: saveButtonMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
if (root.editingDevice && root.newDeviceName.trim() !== "") {
AudioService.setDeviceAlias(root.editingDevice.name, root.newDeviceName);
root.showEditDialog = false;
}
}
}
}
Rectangle {
width: cancelButtonText.width + Theme.spacingL * 2
height: Theme.buttonHeight
radius: Theme.cornerRadius
color: cancelButtonMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
border.width: 1
border.color: Theme.outline
StyledText {
id: cancelButtonText
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: cancelButtonMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.showEditDialog = false;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,155 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property var deviceNode
property string deviceType: "output"
signal editRequested(var deviceNode)
signal resetRequested(var deviceNode)
width: parent?.width ?? 0
height: deviceRowContent.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
readonly property bool hasCustomAlias: AudioService.hasDeviceAlias(deviceNode?.name ?? "")
readonly property string displayedName: AudioService.displayName(deviceNode)
Row {
id: deviceRowContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: root.deviceType === "input" ? "mic" : "speaker"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSize - Theme.spacingM * 3 - buttonsRow.width
spacing: 2
StyledText {
text: root.displayedName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: root.deviceNode?.name ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width - (customAliasLabel.visible ? customAliasLabel.width + Theme.spacingS : 0)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: customAliasLabel
visible: root.hasCustomAlias
height: customAliasText.implicitHeight + 4
width: customAliasText.implicitWidth + Theme.spacingS * 2
radius: 3
color: Theme.withAlpha(Theme.primary, 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: customAliasText
text: I18n.tr("Custom")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.primary
font.weight: Font.Medium
anchors.centerIn: parent
}
}
}
StyledText {
visible: root.hasCustomAlias
text: I18n.tr("Original: %1").arg(AudioService.originalName(root.deviceNode))
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
opacity: 0.6
}
}
}
Row {
id: buttonsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
id: resetButton
visible: root.hasCustomAlias
buttonSize: 36
iconName: "restart_alt"
iconSize: 20
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceVariantText
tooltipText: I18n.tr("Reset to default name")
anchors.verticalCenter: parent.verticalCenter
onClicked: {
root.resetRequested(root.deviceNode);
}
}
DankActionButton {
id: editButton
buttonSize: 36
iconName: "edit"
iconSize: 20
backgroundColor: Theme.buttonBg
iconColor: Theme.buttonText
tooltipText: I18n.tr("Set custom name")
anchors.verticalCenter: parent.verticalCenter
onClicked: {
root.editRequested(root.deviceNode);
}
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
z: -1
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -31,8 +31,19 @@ Singleton {
property var mediaDevices: null
property var mediaDevicesConnections: null
property var deviceAliases: ({})
property string wireplumberConfigPath: {
const homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation);
const homePath = homeUrl.toString().replace("file://", "");
return homePath + "/.config/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf";
}
property bool wireplumberReloading: false
signal micMuteChanged
signal audioOutputCycled(string deviceName)
signal deviceAliasChanged(string nodeName, string newAlias)
signal wireplumberReloadStarted()
signal wireplumberReloadCompleted(bool success)
function getAvailableSinks() {
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream);
@@ -60,6 +71,226 @@ Singleton {
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 homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation);
const homePath = homeUrl.toString().replace("file://", "");
const configDir = homePath + "/.config/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 homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation);
const homePath = homeUrl.toString().replace("file://", "");
const configPath = homePath + "/.config/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf";
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
@@ -443,20 +674,40 @@ Singleton {
return "";
}
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"];
// 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 Speakers";
return "Built-in Audio Analog Stereo";
}
if (node.name.includes("bluez")) {
return "Bluetooth Audio";
@@ -471,6 +722,48 @@ Singleton {
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 "";
@@ -658,5 +951,7 @@ Singleton {
checkGsettings();
Qt.callLater(createSoundPlayers);
}
loadDeviceAliases();
}
}