mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42: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:
503
quickshell/Modules/Settings/AudioTab.qml
Normal file
503
quickshell/Modules/Settings/AudioTab.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml
Normal file
155
quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user