diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 94956f48..00ba5fd3 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -123,6 +123,8 @@ Singleton { property string vpnLastConnected: "" property var deviceMaxVolumes: ({}) + property var hiddenOutputDeviceNames: [] + property var hiddenInputDeviceNames: [] Component.onCompleted: { if (!isGreeterMode) { @@ -1069,6 +1071,20 @@ Singleton { saveSettings(); } + function setHiddenOutputDeviceNames(deviceNames) { + if (!Array.isArray(deviceNames)) + return; + hiddenOutputDeviceNames = deviceNames; + saveSettings(); + } + + function setHiddenInputDeviceNames(deviceNames) { + if (!Array.isArray(deviceNames)) + return; + hiddenInputDeviceNames = deviceNames; + saveSettings(); + } + function getDeviceMaxVolume(nodeName) { if (!nodeName) return 100; diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index 6f7c11f0..5e6347ac 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -75,7 +75,9 @@ var SPEC = { vpnLastConnected: { def: "" }, - deviceMaxVolumes: { def: {} } + deviceMaxVolumes: { def: {} }, + hiddenOutputDeviceNames: { def: [] }, + hiddenInputDeviceNames: { def: [] } }; function getValidKeys() { diff --git a/quickshell/Modules/Settings/AudioTab.qml b/quickshell/Modules/Settings/AudioTab.qml index 44a52f33..65f7f0c1 100644 --- a/quickshell/Modules/Settings/AudioTab.qml +++ b/quickshell/Modules/Settings/AudioTab.qml @@ -18,6 +18,22 @@ Item { property string editingDeviceType: "" property string newDeviceName: "" property bool isReloadingAudio: false + property var hiddenOutputDeviceNames: SessionData.hiddenOutputDeviceNames ?? [] + property var hiddenInputDeviceNames: SessionData.hiddenInputDeviceNames ?? [] + property bool showHiddenOutputDevices: false + property bool showHiddenInputDevices: false + + function persistHiddenOutputDeviceNames(deviceNames) { + const uniqueNames = [...new Set(deviceNames)]; + hiddenOutputDeviceNames = uniqueNames; + SessionData.setHiddenOutputDeviceNames(uniqueNames); + } + + function persistHiddenInputDeviceNames(deviceNames) { + const uniqueNames = [...new Set(deviceNames)]; + hiddenInputDeviceNames = uniqueNames; + SessionData.setHiddenInputDeviceNames(uniqueNames); + } function updateDeviceList() { const allNodes = Pipewire.nodes.values; @@ -56,6 +72,8 @@ Item { } Component.onCompleted: { + hiddenOutputDeviceNames = SessionData.hiddenOutputDeviceNames ?? []; + hiddenInputDeviceNames = SessionData.hiddenInputDeviceNames ?? []; updateDeviceList(); } @@ -132,7 +150,7 @@ Item { } Repeater { - model: root.outputDevices + model: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name)) delegate: Column { required property var modelData @@ -142,6 +160,7 @@ Item { DeviceAliasRow { deviceNode: modelData deviceType: "output" + showHideButton: true onEditRequested: device => { root.editingDevice = device; @@ -153,6 +172,10 @@ Item { onResetRequested: device => { AudioService.removeDeviceAlias(device.name); } + + onHideRequested: device => { + root.persistHiddenOutputDeviceNames([...root.hiddenOutputDeviceNames, device.name]); + } } Item { @@ -161,7 +184,7 @@ Item { StyledText { id: maxVolLabel - text: I18n.tr("Max Volume", "Audio settings: maximum volume limit per device") + text: I18n.tr("Max Volume", "Audio settings: maximum volume limit per device") + " ยท " + maxVolSlider.value + "%" anchors.left: parent.left anchors.leftMargin: Theme.spacingM + Theme.iconSize + Theme.spacingM anchors.verticalCenter: parent.verticalCenter @@ -182,6 +205,8 @@ Item { maximum: 200 step: 5 showValue: true + wheelEnabled: false + centerMinimum: true unit: "%" onSliderValueChanged: newValue => { SessionData.setDeviceMaxVolume(modelData.name, newValue); @@ -204,9 +229,87 @@ Item { font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText horizontalAlignment: Text.AlignHCenter - visible: root.outputDevices.length === 0 + visible: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name)).length === 0 && root.hiddenOutputDeviceNames.length === 0 topPadding: Theme.spacingM } + + Column { + width: parent.width + spacing: 0 + visible: root.hiddenOutputDeviceNames.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "visibility_off" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Hidden (%1)").arg(root.hiddenOutputDeviceNames.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + name: root.showHiddenOutputDevices ? "expand_less" : "expand_more" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.showHiddenOutputDevices = !root.showHiddenOutputDevices + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.showHiddenOutputDevices + + Repeater { + model: root.outputDevices.filter(d => root.hiddenOutputDeviceNames.includes(d.name)) + + delegate: DeviceAliasRow { + required property var modelData + deviceNode: modelData + deviceType: "output" + isHidden: true + showHideButton: true + + onHideRequested: device => { + root.persistHiddenOutputDeviceNames(root.hiddenOutputDeviceNames.filter(n => n !== device.name)); + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + } + } + } + } } } @@ -238,13 +341,14 @@ Item { } Repeater { - model: root.inputDevices + model: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name)) delegate: DeviceAliasRow { required property var modelData deviceNode: modelData deviceType: "input" + showHideButton: true onEditRequested: device => { root.editingDevice = device; @@ -256,6 +360,10 @@ Item { onResetRequested: device => { AudioService.removeDeviceAlias(device.name); } + + onHideRequested: device => { + root.persistHiddenInputDeviceNames([...root.hiddenInputDeviceNames, device.name]); + } } } @@ -265,9 +373,87 @@ Item { font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText horizontalAlignment: Text.AlignHCenter - visible: root.inputDevices.length === 0 + visible: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name)).length === 0 && root.hiddenInputDeviceNames.length === 0 topPadding: Theme.spacingM } + + Column { + width: parent.width + spacing: 0 + visible: root.hiddenInputDeviceNames.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "visibility_off" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Hidden (%1)").arg(root.hiddenInputDeviceNames.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + name: root.showHiddenInputDevices ? "expand_less" : "expand_more" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.showHiddenInputDevices = !root.showHiddenInputDevices + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.showHiddenInputDevices + + Repeater { + model: root.inputDevices.filter(d => root.hiddenInputDeviceNames.includes(d.name)) + + delegate: DeviceAliasRow { + required property var modelData + deviceNode: modelData + deviceType: "input" + isHidden: true + showHideButton: true + + onHideRequested: device => { + root.persistHiddenInputDeviceNames(root.hiddenInputDeviceNames.filter(n => n !== device.name)); + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + } + } + } + } } } } diff --git a/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml b/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml index a940bf33..bec5ff1f 100644 --- a/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml +++ b/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml @@ -14,8 +14,12 @@ Rectangle { required property var deviceNode property string deviceType: "output" + property bool showHideButton: false + property bool isHidden: false + signal editRequested(var deviceNode) signal resetRequested(var deviceNode) + signal hideRequested(var deviceNode) width: parent?.width ?? 0 height: deviceRowContent.height + Theme.spacingM * 2 @@ -127,6 +131,21 @@ Rectangle { } } + DankActionButton { + id: hideButton + visible: root.showHideButton + buttonSize: 36 + iconName: root.isHidden ? "visibility" : "visibility_off" + iconSize: 20 + backgroundColor: Theme.surfaceContainerHigh + iconColor: root.isHidden ? Theme.primary : Theme.surfaceVariantText + tooltipText: root.isHidden ? I18n.tr("Show device") : I18n.tr("Hide device") + anchors.verticalCenter: parent.verticalCenter + onClicked: { + root.hideRequested(root.deviceNode); + } + } + DankActionButton { id: editButton buttonSize: 36 @@ -136,6 +155,7 @@ Rectangle { iconColor: Theme.buttonText tooltipText: I18n.tr("Set custom name") anchors.verticalCenter: parent.verticalCenter + visible: !root.isHidden onClicked: { root.editRequested(root.deviceNode); } diff --git a/quickshell/Widgets/DankSlider.qml b/quickshell/Widgets/DankSlider.qml index c161b214..538c0a9f 100644 --- a/quickshell/Widgets/DankSlider.qml +++ b/quickshell/Widgets/DankSlider.qml @@ -16,6 +16,7 @@ Item { property bool showValue: true property bool isDragging: false property bool wheelEnabled: true + property bool centerMinimum: false property real valueOverride: -1 property bool alwaysShowValue: false readonly property bool containsMouse: sliderMouseArea.containsMouse @@ -30,6 +31,8 @@ Item { function updateValueFromPosition(x) { let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width))); + if (centerMinimum) + ratio = Math.max(0, (ratio - 0.5) * 2); let rawValue = minimum + ratio * (maximum - minimum); let newValue = step > 1 ? Math.round(rawValue / step) * step : Math.round(rawValue); newValue = Math.max(minimum, Math.min(maximum, newValue)); @@ -70,7 +73,9 @@ Item { height: parent.height radius: Theme.cornerRadius width: { - const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum); + const range = slider.maximum - slider.minimum; + const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range; + const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio; const travel = sliderTrack.width - sliderHandle.width; const center = (travel * ratio) + sliderHandle.width / 2; return Math.max(0, Math.min(sliderTrack.width, center)); @@ -87,7 +92,9 @@ Item { height: 24 radius: Theme.cornerRadius x: { - const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum); + const range = slider.maximum - slider.minimum; + const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range; + const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio; const travel = sliderTrack.width - width; return Math.max(0, Math.min(travel, travel * ratio)); }