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:
parent
e4d4c457f8
commit
9ada66a78e
10 changed files with 277 additions and 154 deletions
|
|
@ -25,7 +25,7 @@ Slider {
|
|||
implicitHeight: parent.height - y
|
||||
|
||||
color: Colours.alpha(Colours.palette.m3secondary, true)
|
||||
radius: Appearance.rounding.full
|
||||
radius: parent.radius
|
||||
}
|
||||
}
|
||||
|
||||
58
components/controls/StyledRadioButton.qml
Normal file
58
components/controls/StyledRadioButton.qml
Normal 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
|
||||
}
|
||||
}
|
||||
59
components/controls/StyledSlider.qml
Normal file
59
components/controls/StyledSlider.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ Item {
|
|||
|
||||
sourceComponent: MaterialIcon {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,139 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
ColumnLayout {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var wrapper
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
VerticalSlider {
|
||||
id: volumeSlider
|
||||
|
||||
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
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: pavuButton
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.wrapper.hasCurrent = false;
|
||||
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]);
|
||||
StyledText {
|
||||
Layout.bottomMargin: Appearance.spacing.small / 2
|
||||
text: qsTr("Output")
|
||||
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 {
|
||||
id: icon
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
Layout.bottomMargin: Appearance.spacing.small / 2
|
||||
text: qsTr("Input")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "settings"
|
||||
Repeater {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.components.misc
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
|
|
@ -279,9 +280,10 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Slider {
|
||||
StyledSlider {
|
||||
id: slider
|
||||
|
||||
enabled: !!Players.active
|
||||
implicitWidth: controls.implicitWidth * 1.5
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
|
|
@ -291,56 +293,6 @@ Item {
|
|||
if (active?.canSeek && active?.positionSupported)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
|
|
@ -164,7 +165,7 @@ RowLayout {
|
|||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Slider {
|
||||
StyledSlider {
|
||||
id: slider
|
||||
|
||||
Layout.rightMargin: root.isLarge ? Appearance.spacing.small : 0
|
||||
|
|
@ -177,56 +178,6 @@ RowLayout {
|
|||
if (active?.canSeek && active?.positionSupported)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
|
|
@ -26,18 +27,10 @@ Column {
|
|||
Audio.setVolume(Audio.volume - 0.1);
|
||||
}
|
||||
|
||||
VerticalSlider {
|
||||
FilledSlider {
|
||||
anchors.fill: parent
|
||||
|
||||
icon: {
|
||||
if (Audio.muted)
|
||||
return "no_sound";
|
||||
if (value >= 0.5)
|
||||
return "volume_up";
|
||||
if (value > 0)
|
||||
return "volume_down";
|
||||
return "volume_mute";
|
||||
}
|
||||
icon: Icons.getVolumeIcon(value, Audio.muted)
|
||||
value: Audio.volume
|
||||
onMoved: Audio.setVolume(value)
|
||||
}
|
||||
|
|
@ -57,7 +50,7 @@ Column {
|
|||
monitor.setBrightness(monitor.brightness - 0.1);
|
||||
}
|
||||
|
||||
VerticalSlider {
|
||||
FilledSlider {
|
||||
anchors.fill: parent
|
||||
|
||||
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
||||
|
|
|
|||
|
|
@ -6,20 +6,39 @@ import Quickshell.Services.Pipewire
|
|||
Singleton {
|
||||
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 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
|
||||
|
||||
function setVolume(volume: real): void {
|
||||
function setVolume(newVolume: real): void {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
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 {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
objects: [...root.sinks, ...root.sources]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,16 @@ Singleton {
|
|||
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 {
|
||||
path: "/etc/os-release"
|
||||
onLoaded: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue