mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-11 00:02:28 -04:00
feat: Alias for Audio Devices
- New custom audio UI to set custom names for input/output devices
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user