earbang protection

This commit is contained in:
end-4
2025-06-07 19:43:05 +02:00
parent c1a4670de2
commit 57327ab0fe
4 changed files with 114 additions and 20 deletions
@@ -12,6 +12,14 @@ Singleton {
property int fakeScreenRounding: 1 // 0: None | 1: Always | 2: When not fullscreen
}
property QtObject audio: QtObject { // Values in %
property QtObject protection: QtObject { // Prevent sudden bangs
property bool enable: true
property real maxAllowedIncrease: 10
property real maxAllowed: 90 // Realistically should already provide some protection when it's 99...
}
}
property QtObject apps: QtObject {
property string bluetooth: "better-control --bluetooth"
property string imageViewer: "loupe"
@@ -73,7 +73,7 @@ Scope {
item: osdValuesWrapper
}
implicitWidth: Appearance.sizes.osdWidth
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
visible: osdLoader.active
@@ -12,6 +12,7 @@ import Quickshell.Hyprland
Scope {
id: root
property bool showOsdValues: false
property string protectionMessage: ""
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
function triggerOsd() {
@@ -25,7 +26,8 @@ Scope {
repeat: false
running: false
onTriggered: {
showOsdValues = false
root.showOsdValues = false
root.protectionMessage = ""
}
}
@@ -36,7 +38,7 @@ Scope {
}
}
Connections {
Connections { // Listen to volume changes
target: Audio.sink?.audio ?? null
function onVolumeChanged() {
if (!Audio.ready) return
@@ -48,6 +50,14 @@ Scope {
}
}
Connections { // Listen to protection triggers
target: Audio
function onSinkProtectionTriggered(reason) {
root.protectionMessage = reason;
root.triggerOsd()
}
}
Loader {
id: osdLoader
active: showOsdValues
@@ -75,7 +85,7 @@ Scope {
item: osdValuesWrapper
}
implicitWidth: Appearance.sizes.osdWidth
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
visible: osdLoader.active
@@ -85,8 +95,8 @@ Scope {
Item {
id: osdValuesWrapper
// Extra space for shadow
implicitHeight: osdValues.implicitHeight + Appearance.sizes.elevationMargin * 2
implicitWidth: osdValues.implicitWidth
implicitHeight: contentColumnLayout.implicitHeight + Appearance.sizes.elevationMargin * 2
implicitWidth: contentColumnLayout.implicitWidth
clip: true
MouseArea {
@@ -95,20 +105,63 @@ Scope {
onEntered: root.showOsdValues = false
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.animation.menuDecel.duration
easing.type: Appearance.animation.menuDecel.type
ColumnLayout {
id: contentColumnLayout
anchors {
top: parent.top
left: parent.left
right: parent.right
leftMargin: Appearance.sizes.elevationMargin
rightMargin: Appearance.sizes.elevationMargin
}
}
spacing: 0
OsdValueIndicator {
id: osdValues
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
value: Audio.sink?.audio.volume ?? 0
icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
name: qsTr("Volume")
OsdValueIndicator {
id: osdValues
Layout.fillWidth: true
value: Audio.sink?.audio.volume ?? 0
icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
name: qsTr("Volume")
}
Item {
id: protectionMessageWrapper
implicitHeight: protectionMessageBackground.implicitHeight
implicitWidth: protectionMessageBackground.implicitWidth
Layout.alignment: Qt.AlignHCenter
opacity: root.protectionMessage !== "" ? 1 : 0
StyledRectangularShadow {
target: protectionMessageBackground
}
Rectangle {
id: protectionMessageBackground
anchors.centerIn: parent
color: Appearance.m3colors.m3error
property real padding: 10
implicitHeight: protectionMessageRowLayout.implicitHeight + padding * 2
implicitWidth: protectionMessageRowLayout.implicitWidth + padding * 2
radius: Appearance.rounding.normal
RowLayout {
id: protectionMessageRowLayout
anchors.centerIn: parent
MaterialSymbol {
id: protectionMessageIcon
text: "dangerous"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onError
}
StyledText {
id: protectionMessageTextWidget
horizontalAlignment: Text.AlignHCenter
color: Appearance.m3colors.m3onError
wrapMode: Text.Wrap
text: root.protectionMessage
}
}
}
}
}
}
}
+35 -2
View File
@@ -1,3 +1,4 @@
import "root:/modules/common"
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
@@ -11,11 +12,43 @@ Singleton {
id: root
property bool ready: Pipewire.defaultAudioSink?.ready ?? false
property var sink: Pipewire.defaultAudioSink
property var source: Pipewire.defaultAudioSource
property PwNode sink: Pipewire.defaultAudioSink
property PwNode source: Pipewire.defaultAudioSource
signal sinkProtectionTriggered(string reason);
PwObjectTracker {
objects: [sink, source]
Component.onCompleted: {
sink.audio.volume = sink.audio.volume; // Trigger initial volume change
}
}
Connections { // Protection against sudden volume changes
target: sink?.audio ?? null
property bool lastReady: false
property real lastVolume: 0
function onVolumeChanged() {
if (!ConfigOptions.audio.protection.enable) return;
if (!lastReady) {
lastVolume = sink.audio.volume;
lastReady = true;
return;
}
const newVolume = sink.audio.volume;
const maxAllowedIncrease = ConfigOptions.audio.protection.maxAllowedIncrease / 100;
const maxAllowed = ConfigOptions.audio.protection.maxAllowed / 100;
if (newVolume - lastVolume > maxAllowedIncrease) {
sink.audio.volume = lastVolume;
root.sinkProtectionTriggered("Illegal increment");
} else if (newVolume > maxAllowed) {
sink.audio.volume = lastVolume;
root.sinkProtectionTriggered("Exceeded max allowed");
}
lastVolume = sink.audio.volume;
}
}
}