feat: New audio panel when clicking on volume icon

This commit is contained in:
ItsLemmy
2025-10-26 16:04:28 -04:00
parent 2ad5c8352e
commit 06d095c705
7 changed files with 273 additions and 23 deletions

View File

@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
property real localOutputVolume: AudioService.volume || 0
property bool localOutputVolumeChanging: false
property real localInputVolume: AudioService.inputVolume || 0
property bool localInputVolumeChanging: false
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
panelKeyboardFocus: true
// Connections to update local volumes when AudioService changes
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
if (!localOutputVolumeChanging) {
localOutputVolume = AudioService.volume
}
}
}
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
if (!localInputVolumeChanging) {
localInputVolume = AudioService.inputVolume
}
}
}
// Timer to debounce volume changes
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localOutputVolume)
}
if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) {
AudioService.setInputVolume(localInputVolume)
}
}
}
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: "settings-audio"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("settings.audio.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: AudioService.getOutputIcon()
tooltipText: I18n.tr("tooltips.output-devices")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setOutputMuted(!AudioService.muted)
}
}
NIconButton {
icon: AudioService.getInputIcon()
tooltipText: I18n.tr("tooltips.refresh-devices")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
}
}
NDivider {
Layout.fillWidth: true
}
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// AudioService Devices
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
RowLayout {
spacing: Style.spacingM * Style.uiScaling
Layout.bottomMargin: Style.marginL
NText {
text: I18n.tr("settings.audio.devices.output-device.label")
pointSize: Style.fontSizeL
color: Color.mPrimary
Layout.preferredWidth: root.preferredWidth * 0.3
}
// Output Volume Slider
NValueSlider {
Layout.fillWidth: true
Layout.maximumWidth: root.preferredWidth * 0.6
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localOutputVolume
stepSize: 0.01
heightRatio: 0.5
onMoved: value => localOutputVolume = value
onPressedChanged: (pressed, value) => localOutputVolumeChanging = pressed
text: Math.round(localOutputVolume * 100) + "%"
}
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
pointSize: Style.fontSizeS
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData)
localVolume = AudioService.volume
}
Layout.fillWidth: true
}
}
}
NDivider {
Layout.fillWidth: true
}
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
RowLayout {
spacing: Style.spacingM * Style.uiScaling
Layout.bottomMargin: Style.marginL
NText {
text: I18n.tr("settings.audio.devices.input-device.label")
pointSize: Style.fontSizeL
color: Color.mPrimary
Layout.preferredWidth: root.preferredWidth * 0.3
}
// Input Volume Slider
NValueSlider {
Layout.fillWidth: true
Layout.maximumWidth: root.preferredWidth * 0.6
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localInputVolume
stepSize: 0.01
heightRatio: 0.5
onMoved: value => localInputVolume = value
onPressedChanged: (pressed, value) => localInputVolumeChanging = pressed
text: Math.round(localInputVolume * 100) + "%"
}
}
Repeater {
model: AudioService.sources
//Layout.fillWidth: true
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
pointSize: Style.fontSizeS
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}

View File

@@ -40,13 +40,6 @@ Item {
implicitWidth: pill.width
implicitHeight: pill.height
function getIcon() {
if (AudioService.inputMuted) {
return "microphone-mute"
}
return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
}
// Connection used to open the pill when input volume changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
@@ -90,7 +83,7 @@ Item {
id: pill
oppositeDirection: BarService.getPillDirection(root)
icon: getIcon()
icon: AudioService.getInputIcon()
density: Settings.data.bar.density
autoHide: false // Important to be false so we can hover as long as we want
text: Math.round(AudioService.inputVolume * 100)

View File

@@ -40,13 +40,6 @@ Item {
implicitWidth: pill.width
implicitHeight: pill.height
function getIcon() {
if (AudioService.muted) {
return "volume-mute"
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
}
// Connection used to open the pill when volume changes
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
@@ -76,7 +69,7 @@ Item {
density: Settings.data.bar.density
oppositeDirection: BarService.getPillDirection(root)
icon: getIcon()
icon: AudioService.getOutputIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: Math.round(AudioService.volume * 100)
suffix: "%"
@@ -100,9 +93,7 @@ Item {
AudioService.setOutputMuted(!AudioService.muted)
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open()
PanelService.getPanel("audioPanel")?.toggle(this)
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])

View File

@@ -148,4 +148,18 @@ Singleton {
root._inputVolume = newSource?.audio?.volume ?? 0
root._inputMuted = !!newSource?.audio?.muted
}
function getOutputIcon() {
if (muted) {
return "volume-mute"
}
return (volume <= Number.EPSILON) ? "volume-zero" : (volume <= 0.5) ? "volume-low" : "volume-high"
}
function getInputIcon() {
if (inputMuted) {
return "microphone-mute"
}
return (inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
}
}

View File

@@ -7,11 +7,13 @@ import qs.Widgets
RadioButton {
id: root
property real pointSize: Style.fontSizeM
indicator: Rectangle {
id: outerCircle
implicitWidth: Style.baseWidgetSize * 0.625
implicitHeight: Style.baseWidgetSize * 0.625
implicitWidth: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM
implicitHeight: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM
radius: width * 0.5
color: Color.transparent
border.color: root.checked ? Color.mPrimary : Color.mOnSurface
@@ -41,7 +43,7 @@ RadioButton {
contentItem: NText {
text: root.text
pointSize: Style.fontSizeM
pointSize: root.pointSize
anchors.verticalCenter: parent.verticalCenter
anchors.left: outerCircle.right
anchors.right: parent.right

View File

@@ -47,7 +47,7 @@ RowLayout {
pointSize: root.textSize
family: Settings.data.ui.fontFixed
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 45 * Scale.uiScaleRatio
Layout.preferredWidth: 45 * Style.uiScaleRatio
horizontalAlignment: Text.AlignRight
}
}

View File

@@ -27,6 +27,7 @@ import qs.Modules.SessionMenu
// Bar & Bar Components
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Bar.Audio
import qs.Modules.Bar.Bluetooth
import qs.Modules.Bar.Battery
import qs.Modules.Bar.Calendar
@@ -168,6 +169,11 @@ ShellRoot {
objectName: "bluetoothPanel"
}
AudioPanel {
id: audioPanel
objectName: "audioPanel"
}
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel"