bar/popouts: add audio device switcher (#319)

* feat: basic audio switcher

* feat: replace VerticalSlider with StyledSlider

* fix: styling

* fix: formatting

* chore: make sound icons consistent, change slider styling

* feat: styled slider component variants

* chore: cleanup

* chore: cleanup

* fix: pr fixes

* fix: remove redundant code

* chore: remove old code

* fix: controls styling

* fixes

* more tweaks

* radiobtn: add interaction stuff

Anim slider

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
This commit is contained in:
Laurens Duin 2025-08-05 08:04:53 +02:00 committed by GitHub
parent e4d4c457f8
commit 9ada66a78e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 277 additions and 154 deletions

View file

@ -25,7 +25,7 @@ Slider {
implicitHeight: parent.height - y implicitHeight: parent.height - y
color: Colours.alpha(Colours.palette.m3secondary, true) color: Colours.alpha(Colours.palette.m3secondary, true)
radius: Appearance.rounding.full radius: parent.radius
} }
} }

View file

@ -0,0 +1,58 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
RadioButton {
id: root
font.pointSize: Appearance.font.size.smaller
indicator: Rectangle {
id: outerCircle
implicitWidth: 20
implicitHeight: 20
radius: Appearance.rounding.full
color: "transparent"
border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
border.width: 2
anchors.verticalCenter: parent.verticalCenter
StateLayer {
anchors.margins: -Appearance.padding.smaller
color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary
z: -1
function onClicked(): void {
root.click();
}
}
StyledRect {
anchors.centerIn: parent
implicitWidth: 8
implicitHeight: 8
radius: Appearance.rounding.full
color: root.checked ? Colours.palette.m3primary : "transparent"
}
Behavior on border.color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
contentItem: StyledText {
text: root.text
font.pointSize: root.font.pointSize
anchors.verticalCenter: parent.verticalCenter
anchors.left: outerCircle.right
anchors.leftMargin: Appearance.spacing.smaller
}
}

View file

@ -0,0 +1,59 @@
import qs.components
import qs.config
import qs.services
import QtQuick.Controls
import QtQuick
Slider {
id: slider
background: Item {
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: slider.handle.x - slider.implicitHeight / 6
color: Colours.palette.m3primary
radius: Appearance.rounding.full
topRightRadius: slider.implicitHeight / 15
bottomRightRadius: slider.implicitHeight / 15
}
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
topLeftRadius: slider.implicitHeight / 15
bottomLeftRadius: slider.implicitHeight / 15
}
}
handle: StyledRect {
id: rect
x: slider.visualPosition * slider.availableWidth
implicitWidth: slider.implicitHeight / 4.5
implicitHeight: slider.implicitHeight
color: Colours.palette.m3primary
radius: Appearance.rounding.full
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: event => event.accepted = false
}
}
}

View file

@ -58,7 +58,7 @@ Item {
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
animate: true animate: true
text: Audio.muted ? "volume_off" : Audio.volume >= 0.66 ? "volume_up" : Audio.volume >= 0.33 ? "volume_down" : "volume_mute" text: Icons.getVolumeIcon(Audio.volume, Audio.muted)
color: root.colour color: root.colour
} }
} }

View file

@ -1,58 +1,139 @@
pragma ComponentBehavior: Bound
import qs.components import qs.components
import qs.components.controls import qs.components.controls
import qs.services import qs.services
import qs.config import qs.config
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
ColumnLayout { Item {
id: root id: root
required property var wrapper required property var wrapper
spacing: Appearance.spacing.normal implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2
VerticalSlider { ButtonGroup {
id: volumeSlider id: sinks
icon: {
if (Audio.muted)
return "no_sound";
if (value >= 0.5)
return "volume_up";
if (value > 0)
return "volume_down";
return "volume_mute";
}
value: Audio.volume
onMoved: Audio.setVolume(value)
implicitWidth: Config.osd.sizes.sliderWidth
implicitHeight: Config.osd.sizes.sliderHeight
} }
StyledRect { ButtonGroup {
id: pavuButton id: sources
}
implicitWidth: implicitHeight ColumnLayout {
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 id: layout
radius: Appearance.rounding.normal anchors.left: parent.left
color: Colours.palette.m3surfaceContainer anchors.verticalCenter: parent.verticalCenter
spacing: 0
StateLayer { StyledText {
function onClicked(): void { Layout.bottomMargin: Appearance.spacing.small / 2
root.wrapper.hasCurrent = false; text: qsTr("Output")
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]); font.weight: 500
}
Repeater {
model: Audio.sinks
StyledRadioButton {
id: control
required property PwNode modelData
ButtonGroup.group: sinks
checked: Audio.sink?.id === modelData.id
onClicked: Audio.setAudioSink(modelData)
text: modelData.description
} }
} }
MaterialIcon { StyledText {
id: icon Layout.topMargin: Appearance.spacing.small
Layout.bottomMargin: Appearance.spacing.small / 2
text: qsTr("Input")
font.weight: 500
}
anchors.centerIn: parent Repeater {
text: "settings" model: Audio.sources
StyledRadioButton {
required property PwNode modelData
ButtonGroup.group: sources
checked: Audio.source?.id === modelData.id
onClicked: Audio.setAudioSource(modelData)
text: modelData.description
}
}
StyledText {
Layout.topMargin: Appearance.spacing.small
Layout.bottomMargin: Appearance.spacing.small / 2
text: qsTr("Volume")
font.weight: 500
}
StyledSlider {
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
value: Audio.volume
onMoved: Audio.setVolume(value)
Behavior on value {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledRect {
Layout.topMargin: Appearance.spacing.normal
visible: Config.general.apps.audio.length > 0
implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2
implicitHeight: expandBtn.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.normal
color: Colours.palette.m3primaryContainer
StateLayer {
color: Colours.palette.m3onPrimaryContainer
function onClicked(): void {
root.wrapper.hasCurrent = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]);
}
}
RowLayout {
id: expandBtn
anchors.centerIn: parent
spacing: Appearance.spacing.small
StyledText {
Layout.leftMargin: Appearance.padding.smaller
text: qsTr("Open settings")
color: Colours.palette.m3onPrimaryContainer
}
MaterialIcon {
text: "chevron_right"
color: Colours.palette.m3onPrimaryContainer
font.pointSize: Appearance.font.size.large
}
}
} }
} }
} }

View file

@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import qs.components import qs.components
import qs.components.effects import qs.components.effects
import qs.components.misc import qs.components.misc
import qs.components.controls
import qs.services import qs.services
import qs.utils import qs.utils
import qs.config import qs.config
@ -279,9 +280,10 @@ Item {
} }
} }
Slider { StyledSlider {
id: slider id: slider
enabled: !!Players.active
implicitWidth: controls.implicitWidth * 1.5 implicitWidth: controls.implicitWidth * 1.5
implicitHeight: Appearance.padding.normal * 3 implicitHeight: Appearance.padding.normal * 3
@ -291,56 +293,6 @@ Item {
if (active?.canSeek && active?.positionSupported) if (active?.canSeek && active?.positionSupported)
active.position = value * active.length; active.position = value * active.length;
} }
background: Item {
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: slider.handle.x - slider.implicitHeight / 6
color: Colours.palette.m3primary
radius: Appearance.rounding.full
topRightRadius: slider.implicitHeight / 15
bottomRightRadius: slider.implicitHeight / 15
}
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
topLeftRadius: slider.implicitHeight / 15
bottomLeftRadius: slider.implicitHeight / 15
}
}
handle: StyledRect {
id: rect
x: slider.visualPosition * slider.availableWidth
implicitWidth: slider.implicitHeight / 4.5
implicitHeight: slider.implicitHeight
color: Colours.palette.m3primary
radius: Appearance.rounding.full
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: event => event.accepted = false
}
}
} }
Item { Item {

View file

@ -1,4 +1,5 @@
import qs.components import qs.components
import qs.components.controls
import qs.services import qs.services
import qs.config import qs.config
import Quickshell.Widgets import Quickshell.Widgets
@ -164,7 +165,7 @@ RowLayout {
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
Slider { StyledSlider {
id: slider id: slider
Layout.rightMargin: root.isLarge ? Appearance.spacing.small : 0 Layout.rightMargin: root.isLarge ? Appearance.spacing.small : 0
@ -177,56 +178,6 @@ RowLayout {
if (active?.canSeek && active?.positionSupported) if (active?.canSeek && active?.positionSupported)
active.position = value * active.length; active.position = value * active.length;
} }
background: Item {
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: slider.handle.x - slider.implicitHeight / 6
color: Colours.palette.m3primary
radius: Appearance.rounding.full
topRightRadius: slider.implicitHeight / 15
bottomRightRadius: slider.implicitHeight / 15
}
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
topLeftRadius: slider.implicitHeight / 15
bottomLeftRadius: slider.implicitHeight / 15
}
}
handle: StyledRect {
id: rect
x: slider.visualPosition * slider.availableWidth
implicitWidth: slider.implicitHeight / 4.5
implicitHeight: slider.implicitHeight
color: Colours.palette.m3primary
radius: Appearance.rounding.full
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: event => event.accepted = false
}
}
} }
Control { Control {

View file

@ -1,6 +1,7 @@
import qs.components.controls import qs.components.controls
import qs.services import qs.services
import qs.config import qs.config
import qs.utils
import QtQuick import QtQuick
Column { Column {
@ -26,18 +27,10 @@ Column {
Audio.setVolume(Audio.volume - 0.1); Audio.setVolume(Audio.volume - 0.1);
} }
VerticalSlider { FilledSlider {
anchors.fill: parent anchors.fill: parent
icon: { icon: Icons.getVolumeIcon(value, Audio.muted)
if (Audio.muted)
return "no_sound";
if (value >= 0.5)
return "volume_up";
if (value > 0)
return "volume_down";
return "volume_mute";
}
value: Audio.volume value: Audio.volume
onMoved: Audio.setVolume(value) onMoved: Audio.setVolume(value)
} }
@ -57,7 +50,7 @@ Column {
monitor.setBrightness(monitor.brightness - 0.1); monitor.setBrightness(monitor.brightness - 0.1);
} }
VerticalSlider { FilledSlider {
anchors.fill: parent anchors.fill: parent
icon: `brightness_${(Math.round(value * 6) + 1)}` icon: `brightness_${(Math.round(value * 6) + 1)}`

View file

@ -6,20 +6,39 @@ import Quickshell.Services.Pipewire
Singleton { Singleton {
id: root id: root
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
if (!node.isStream) {
if (node.isSink) acc.sinks.push(node)
else if (node.audio) acc.sources.push(node)
}
return acc
}, { sources: [], sinks: [] })
readonly property list<PwNode> sinks: nodes.sinks
readonly property list<PwNode> sources: nodes.sources
readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool muted: sink?.audio?.muted ?? false readonly property bool muted: !!sink?.audio?.muted
readonly property real volume: sink?.audio?.volume ?? 0 readonly property real volume: sink?.audio?.volume ?? 0
function setVolume(volume: real): void { function setVolume(newVolume: real): void {
if (sink?.ready && sink?.audio) { if (sink?.ready && sink?.audio) {
sink.audio.muted = false; sink.audio.muted = false;
sink.audio.volume = volume; sink.audio.volume = newVolume;
} }
} }
function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink
}
function setAudioSource(newSource: PwNode): void {
Pipewire.preferredDefaultAudioSource = newSource
}
PwObjectTracker { PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource] objects: [...root.sinks, ...root.sources]
} }
} }

View file

@ -223,6 +223,16 @@ Singleton {
return "chat"; return "chat";
} }
function getVolumeIcon(volume: real, isMuted: bool): string {
if (isMuted)
return "no_sound";
if (volume >= 0.5)
return "volume_up";
if (volume > 0)
return "volume_down";
return "volume_mute";
}
FileView { FileView {
path: "/etc/os-release" path: "/etc/os-release"
onLoaded: { onLoaded: {