1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -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 var deviceMaxVolumes: ({})
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings();
@@ -1052,6 +1054,35 @@ Singleton {
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() {
if (!perModeWallpaper)
return;

View File

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

View File

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

View File

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

View File

@@ -140,8 +140,9 @@ BasePill {
volumeAccumulator = 0;
}
const maxVol = AudioService.sinkMaxVolume;
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.volume = newVolume / 100;
AudioService.playVolumeChangeSoundIfEnabled();

View File

@@ -201,8 +201,9 @@ Item {
function adjustVolume(step) {
if (!volumeAvailable)
return;
const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
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();
if (usePlayerVolume) {
@@ -778,7 +779,8 @@ Item {
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
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) {
activePlayer.volume = newVolume / 100;

View File

@@ -95,7 +95,7 @@ DankOSD {
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
maximum: AudioService.sinkMaxVolume
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
@@ -105,7 +105,7 @@ DankOSD {
Component.onCompleted: {
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() {
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
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 {
id: vertTrack
@@ -193,7 +193,7 @@ DankOSD {
Rectangle {
id: vertFill
width: parent.width
height: (vertSlider.value / 100) * parent.height
height: (vertSlider.value / AudioService.sinkMaxVolume) * parent.height
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
@@ -206,7 +206,7 @@ DankOSD {
height: 8
radius: Theme.cornerRadius
y: {
const ratio = vertSlider.value / 100;
const ratio = vertSlider.value / AudioService.sinkMaxVolume;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
@@ -249,8 +249,9 @@ DankOSD {
function updateVolume(mouse) {
if (AudioService.sink && AudioService.sink.audio) {
const maxVol = AudioService.sinkMaxVolume;
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();
AudioService.sink.audio.volume = volume / 100;
resetHideTimer();
@@ -262,7 +263,7 @@ DankOSD {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
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) {
const slider = contentLoader.item.item.children[0].children[1];
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.Controls
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
@@ -131,21 +130,64 @@ Item {
Repeater {
model: root.outputDevices
delegate: DeviceAliasRow {
delegate: Column {
required property var modelData
width: parent?.width ?? 0
spacing: 0
deviceNode: modelData
deviceType: "output"
DeviceAliasRow {
deviceNode: modelData
deviceType: "output"
onEditRequested: device => {
root.editingDevice = device;
root.editingDeviceType = "output";
root.newDeviceName = AudioService.displayName(device);
root.showEditDialog = true;
onEditRequested: device => {
root.editingDevice = device;
root.editingDeviceType = "output";
root.newDeviceName = AudioService.displayName(device);
root.showEditDialog = true;
}
onResetRequested: device => {
AudioService.removeDeviceAlias(device.name);
}
}
onResetRequested: device => {
AudioService.removeDeviceAlias(device.name);
Item {
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
readonly property int sinkMaxVolume: {
const name = sink?.name ?? "";
if (!name)
return 100;
return SessionData.deviceMaxVolumes[name] ?? 100;
}
signal micMuteChanged
signal audioOutputCycled(string deviceName)
signal deviceAliasChanged(string nodeName, string newAlias)
signal wireplumberReloadStarted()
signal wireplumberReloadStarted
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() {
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream);
}
@@ -814,11 +839,11 @@ EOFCONFIG
}
function setVolume(percentage) {
if (!root.sink?.audio) {
if (!root.sink?.audio)
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;
return `Volume set to ${clampedVolume}%`;
}
@@ -859,34 +884,32 @@ EOFCONFIG
}
function increment(step: string): string {
if (!root.sink?.audio) {
if (!root.sink?.audio)
return "No audio sink available";
}
if (root.sink.audio.muted) {
if (root.sink.audio.muted)
root.sink.audio.muted = false;
}
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
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;
return `Volume increased to ${newVolume}%`;
}
function decrement(step: string): string {
if (!root.sink?.audio) {
if (!root.sink?.audio)
return "No audio sink available";
}
if (root.sink.audio.muted) {
if (root.sink.audio.muted)
root.sink.audio.muted = false;
}
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
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;
return `Volume decreased to ${newVolume}%`;
@@ -912,7 +935,8 @@ EOFCONFIG
if (root.sink?.audio) {
const volume = Math.round(root.sink.audio.volume * 100);
const muteStatus = root.sink.audio.muted ? " (muted)" : "";
result += `Output: ${volume}%${muteStatus}\n`;
const maxVol = root.sinkMaxVolume;
result += `Output: ${volume}%${muteStatus} (max: ${maxVol}%)\n`;
} else {
result += "Output: No sink available\n";
}
@@ -928,6 +952,36 @@ EOFCONFIG
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 {
const result = root.cycleAudioOutput();
if (!result)