1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 17:52:10 -04:00

audio: add per-device max volume limit setting

This commit is contained in:
bbedward
2026-02-09 09:26:34 -05:00
parent fce120fa31
commit 1ed44ee6f3
10 changed files with 197 additions and 63 deletions

View File

@@ -121,6 +121,8 @@ Singleton {
property string vpnLastConnected: "" property string vpnLastConnected: ""
property var deviceMaxVolumes: ({})
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings(); loadSettings();
@@ -1052,6 +1054,35 @@ Singleton {
saveSettings(); saveSettings();
} }
function setDeviceMaxVolume(nodeName, maxPercent) {
if (!nodeName)
return;
const updated = Object.assign({}, deviceMaxVolumes);
const clamped = Math.max(100, Math.min(200, Math.round(maxPercent)));
if (clamped === 100) {
delete updated[nodeName];
} else {
updated[nodeName] = clamped;
}
deviceMaxVolumes = updated;
saveSettings();
}
function getDeviceMaxVolume(nodeName) {
if (!nodeName)
return 100;
return deviceMaxVolumes[nodeName] ?? 100;
}
function removeDeviceMaxVolume(nodeName) {
if (!nodeName)
return;
const updated = Object.assign({}, deviceMaxVolumes);
delete updated[nodeName];
deviceMaxVolumes = updated;
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;

View File

@@ -72,7 +72,9 @@ var SPEC = {
appOverrides: { def: {} }, appOverrides: { def: {} },
searchAppActions: { def: true }, searchAppActions: { def: true },
vpnLastConnected: { def: "" } vpnLastConnected: { def: "" },
deviceMaxVolumes: { def: {} }
}; };
function getValidKeys() { function getValidKeys() {

View File

@@ -451,10 +451,11 @@ Column {
if (!AudioService.sink || !AudioService.sink.audio) if (!AudioService.sink || !AudioService.sink.audio)
return; return;
let delta = wheelEvent.angleDelta.y; let delta = wheelEvent.angleDelta.y;
let maxVol = AudioService.sinkMaxVolume;
let currentVolume = AudioService.sink.audio.volume * 100; let currentVolume = AudioService.sink.audio.volume * 100;
let newVolume; let newVolume;
if (delta > 0) if (delta > 0)
newVolume = Math.min(100, currentVolume + 5); newVolume = Math.min(maxVol, currentVolume + 5);
else else
newVolume = Math.max(0, currentVolume - 5); newVolume = Math.max(0, currentVolume - 5);
AudioService.sink.audio.muted = false; AudioService.sink.audio.muted = false;

View File

@@ -102,8 +102,8 @@ Rectangle {
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: AudioService.sink && AudioService.sink.audio enabled: AudioService.sink && AudioService.sink.audio
minimum: 0 minimum: 0
maximum: 100 maximum: AudioService.sinkMaxVolume
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0 value: AudioService.sink && AudioService.sink.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100)) : 0
showValue: true showValue: true
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
@@ -136,15 +136,15 @@ Rectangle {
function normalizePinList(value) { function normalizePinList(value) {
if (Array.isArray(value)) if (Array.isArray(value))
return value.filter(v => v) return value.filter(v => v);
if (typeof value === "string" && value.length > 0) if (typeof value === "string" && value.length > 0)
return [value] return [value];
return [] return [];
} }
function getPinnedOutputs() { function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {} const pins = SettingsData.audioOutputDevicePins || {};
return normalizePinList(pins["preferredOutput"]) return normalizePinList(pins["preferredOutput"]);
} }
Column { Column {
@@ -163,14 +163,14 @@ Rectangle {
let sorted = [...nodes]; let sorted = [...nodes];
sorted.sort((a, b) => { sorted.sort((a, b) => {
// Pinned device first // Pinned device first
const aPinnedIndex = pinnedList.indexOf(a.name) const aPinnedIndex = pinnedList.indexOf(a.name);
const bPinnedIndex = pinnedList.indexOf(b.name) const bPinnedIndex = pinnedList.indexOf(b.name);
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1) if (aPinnedIndex === -1)
return 1 return 1;
if (bPinnedIndex === -1) if (bPinnedIndex === -1)
return -1 return -1;
return aPinnedIndex - bPinnedIndex return aPinnedIndex - bPinnedIndex;
} }
// Then active device // Then active device
if (a === AudioService.sink && b !== AudioService.sink) if (a === AudioService.sink && b !== AudioService.sink)
@@ -292,24 +292,24 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})) const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]) let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
const pinIndex = pinnedList.indexOf(modelData.name) const pinIndex = pinnedList.indexOf(modelData.name);
if (pinIndex !== -1) { if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1) pinnedList.splice(pinIndex, 1);
} else { } else {
pinnedList.unshift(modelData.name) pinnedList.unshift(modelData.name);
if (pinnedList.length > audioContent.maxPinnedOutputs) if (pinnedList.length > audioContent.maxPinnedOutputs)
pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs) pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs);
} }
if (pinnedList.length > 0) if (pinnedList.length > 0)
pins["preferredOutput"] = pinnedList pins["preferredOutput"] = pinnedList;
else else
delete pins["preferredOutput"] delete pins["preferredOutput"];
SettingsData.set("audioOutputDevicePins", pins) SettingsData.set("audioOutputDevicePins", pins);
} }
} }
} }

View File

@@ -69,7 +69,7 @@ Row {
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSink !== null enabled: defaultSink !== null
minimum: 0 minimum: 0
maximum: 100 maximum: AudioService.sinkMaxVolume
showValue: true showValue: true
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
@@ -91,7 +91,7 @@ Row {
Binding { Binding {
target: volumeSlider target: volumeSlider
property: "value" property: "value"
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0 value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
when: !volumeSlider.isDragging when: !volumeSlider.isDragging
} }
} }

View File

@@ -140,8 +140,9 @@ BasePill {
volumeAccumulator = 0; volumeAccumulator = 0;
} }
const maxVol = AudioService.sinkMaxVolume;
const currentVolume = AudioService.sink.audio.volume * 100; const currentVolume = AudioService.sink.audio.volume * 100;
const newVolume = delta > 0 ? Math.min(100, currentVolume + step) : Math.max(0, currentVolume - step); const newVolume = delta > 0 ? Math.min(maxVol, currentVolume + step) : Math.max(0, currentVolume - step);
AudioService.sink.audio.muted = false; AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100; AudioService.sink.audio.volume = newVolume / 100;
AudioService.playVolumeChangeSoundIfEnabled(); AudioService.playVolumeChangeSoundIfEnabled();

View File

@@ -201,8 +201,9 @@ Item {
function adjustVolume(step) { function adjustVolume(step) {
if (!volumeAvailable) if (!volumeAvailable)
return; return;
const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
const current = Math.round(currentVolume * 100); const current = Math.round(currentVolume * 100);
const newVolume = Math.min(100, Math.max(0, current + step)); const newVolume = Math.min(maxVol, Math.max(0, current + step));
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
if (usePlayerVolume) { if (usePlayerVolume) {
@@ -778,7 +779,8 @@ Item {
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0; const current = (currentVolume * 100) || 0;
const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5); const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
const newVolume = delta > 0 ? Math.min(maxVol, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) { if (usePlayerVolume) {
activePlayer.volume = newVolume / 100; activePlayer.volume = newVolume / 100;

View File

@@ -95,7 +95,7 @@ DankOSD {
x: parent.gap * 2 + Theme.iconSize x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
minimum: 0 minimum: 0
maximum: 100 maximum: AudioService.sinkMaxVolume
enabled: AudioService.sink && AudioService.sink.audio enabled: AudioService.sink && AudioService.sink.audio
showValue: true showValue: true
unit: "%" unit: "%"
@@ -105,7 +105,7 @@ DankOSD {
Component.onCompleted: { Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio) { if (AudioService.sink && AudioService.sink.audio) {
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)); value = Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100));
} }
} }
@@ -126,7 +126,7 @@ DankOSD {
function onVolumeChanged() { function onVolumeChanged() {
if (volumeSlider && !volumeSlider.pressed) { if (volumeSlider && !volumeSlider.pressed) {
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)); volumeSlider.value = Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100));
} }
} }
} }
@@ -179,7 +179,7 @@ DankOSD {
y: gap * 2 + Theme.iconSize y: gap * 2 + Theme.iconSize
property bool dragging: false property bool dragging: false
property int value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0 property int value: AudioService.sink && AudioService.sink.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100)) : 0
Rectangle { Rectangle {
id: vertTrack id: vertTrack
@@ -193,7 +193,7 @@ DankOSD {
Rectangle { Rectangle {
id: vertFill id: vertFill
width: parent.width width: parent.width
height: (vertSlider.value / 100) * parent.height height: (vertSlider.value / AudioService.sinkMaxVolume) * parent.height
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary color: Theme.primary
@@ -206,7 +206,7 @@ DankOSD {
height: 8 height: 8
radius: Theme.cornerRadius radius: Theme.cornerRadius
y: { y: {
const ratio = vertSlider.value / 100; const ratio = vertSlider.value / AudioService.sinkMaxVolume;
const travel = parent.height - height; const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio))); return Math.max(0, Math.min(travel, travel * (1 - ratio)));
} }
@@ -249,8 +249,9 @@ DankOSD {
function updateVolume(mouse) { function updateVolume(mouse) {
if (AudioService.sink && AudioService.sink.audio) { if (AudioService.sink && AudioService.sink.audio) {
const maxVol = AudioService.sinkMaxVolume;
const ratio = 1.0 - (mouse.y / height); const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(100, Math.round(ratio * 100))); const volume = Math.max(0, Math.min(maxVol, Math.round(ratio * maxVol)));
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
AudioService.sink.audio.volume = volume / 100; AudioService.sink.audio.volume = volume / 100;
resetHideTimer(); resetHideTimer();
@@ -262,7 +263,7 @@ DankOSD {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() { function onVolumeChanged() {
vertSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)); vertSlider.value = Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100));
} }
} }
} }
@@ -284,7 +285,7 @@ DankOSD {
if (!useVertical) { if (!useVertical) {
const slider = contentLoader.item.item.children[0].children[1]; const slider = contentLoader.item.item.children[0].children[1];
if (slider && slider.value !== undefined) { if (slider && slider.value !== undefined) {
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)); slider.value = Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100));
} }
} }
} }

View File

@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -131,21 +130,64 @@ Item {
Repeater { Repeater {
model: root.outputDevices model: root.outputDevices
delegate: DeviceAliasRow { delegate: Column {
required property var modelData required property var modelData
width: parent?.width ?? 0
spacing: 0
deviceNode: modelData DeviceAliasRow {
deviceType: "output" deviceNode: modelData
deviceType: "output"
onEditRequested: device => { onEditRequested: device => {
root.editingDevice = device; root.editingDevice = device;
root.editingDeviceType = "output"; root.editingDeviceType = "output";
root.newDeviceName = AudioService.displayName(device); root.newDeviceName = AudioService.displayName(device);
root.showEditDialog = true; root.showEditDialog = true;
}
onResetRequested: device => {
AudioService.removeDeviceAlias(device.name);
}
} }
onResetRequested: device => { Item {
AudioService.removeDeviceAlias(device.name); width: parent.width
height: 36
StyledText {
id: maxVolLabel
text: I18n.tr("Max Volume", "Audio settings: maximum volume limit per device")
x: Theme.spacingM + Theme.iconSize + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankSlider {
id: maxVolSlider
anchors.left: maxVolLabel.right
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
height: 36
minimum: 100
maximum: 200
step: 5
showValue: true
unit: "%"
onSliderValueChanged: newValue => {
SessionData.setDeviceMaxVolume(modelData.name, newValue);
}
}
Binding {
target: maxVolSlider
property: "value"
value: SessionData.deviceMaxVolumes[modelData.name] ?? 100
when: !maxVolSlider.isDragging
}
} }
} }
} }

View File

@@ -39,12 +39,37 @@ Singleton {
} }
property bool wireplumberReloading: false property bool wireplumberReloading: false
readonly property int sinkMaxVolume: {
const name = sink?.name ?? "";
if (!name)
return 100;
return SessionData.deviceMaxVolumes[name] ?? 100;
}
signal micMuteChanged signal micMuteChanged
signal audioOutputCycled(string deviceName) signal audioOutputCycled(string deviceName)
signal deviceAliasChanged(string nodeName, string newAlias) signal deviceAliasChanged(string nodeName, string newAlias)
signal wireplumberReloadStarted() signal wireplumberReloadStarted
signal wireplumberReloadCompleted(bool success) 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;
}
}
function getAvailableSinks() { function getAvailableSinks() {
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream); return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream);
} }
@@ -814,11 +839,11 @@ EOFCONFIG
} }
function setVolume(percentage) { function setVolume(percentage) {
if (!root.sink?.audio) { if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
}
const clampedVolume = Math.max(0, Math.min(100, percentage)); const maxVol = root.sinkMaxVolume;
const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
root.sink.audio.volume = clampedVolume / 100; root.sink.audio.volume = clampedVolume / 100;
return `Volume set to ${clampedVolume}%`; return `Volume set to ${clampedVolume}%`;
} }
@@ -859,34 +884,32 @@ EOFCONFIG
} }
function increment(step: string): string { function increment(step: string): string {
if (!root.sink?.audio) { if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
}
if (root.sink.audio.muted) { if (root.sink.audio.muted)
root.sink.audio.muted = false; root.sink.audio.muted = false;
}
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100); const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5"); const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue)); const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue));
root.sink.audio.volume = newVolume / 100; root.sink.audio.volume = newVolume / 100;
return `Volume increased to ${newVolume}%`; return `Volume increased to ${newVolume}%`;
} }
function decrement(step: string): string { function decrement(step: string): string {
if (!root.sink?.audio) { if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
}
if (root.sink.audio.muted) { if (root.sink.audio.muted)
root.sink.audio.muted = false; root.sink.audio.muted = false;
}
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100); const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5"); const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue)); const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue));
root.sink.audio.volume = newVolume / 100; root.sink.audio.volume = newVolume / 100;
return `Volume decreased to ${newVolume}%`; return `Volume decreased to ${newVolume}%`;
@@ -912,7 +935,8 @@ EOFCONFIG
if (root.sink?.audio) { if (root.sink?.audio) {
const volume = Math.round(root.sink.audio.volume * 100); const volume = Math.round(root.sink.audio.volume * 100);
const muteStatus = root.sink.audio.muted ? " (muted)" : ""; const muteStatus = root.sink.audio.muted ? " (muted)" : "";
result += `Output: ${volume}%${muteStatus}\n`; const maxVol = root.sinkMaxVolume;
result += `Output: ${volume}%${muteStatus} (max: ${maxVol}%)\n`;
} else { } else {
result += "Output: No sink available\n"; result += "Output: No sink available\n";
} }
@@ -928,6 +952,36 @@ EOFCONFIG
return result; 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 { function cycleoutput(): string {
const result = root.cycleAudioOutput(); const result = root.cycleAudioOutput();
if (!result) if (!result)