feat: add nexus audio page
This commit is contained in:
parent
a9737915e3
commit
72dc5a8b93
5 changed files with 335 additions and 5 deletions
|
|
@ -15,6 +15,7 @@ Slider {
|
||||||
property bool animateWave
|
property bool animateWave
|
||||||
property real waveFrequency: 6
|
property real waveFrequency: 6
|
||||||
property int waveDuration: 1000
|
property int waveDuration: 1000
|
||||||
|
property int radius: Tokens.rounding.medium
|
||||||
|
|
||||||
property color fgColour: enabled ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3onSurface, 0.38)
|
property color fgColour: enabled ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3onSurface, 0.38)
|
||||||
property color bgColour: enabled ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3onSurface, 0.1)
|
property color bgColour: enabled ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3onSurface, 0.1)
|
||||||
|
|
@ -43,7 +44,7 @@ Slider {
|
||||||
implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1))
|
implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1))
|
||||||
opacity: Math.min(width, 12) / 12
|
opacity: Math.min(width, 12) / 12
|
||||||
|
|
||||||
radius: Tokens.rounding.medium
|
radius: root.radius
|
||||||
topLeftRadius: Tokens.rounding.extraSmall / 2
|
topLeftRadius: Tokens.rounding.extraSmall / 2
|
||||||
bottomLeftRadius: Tokens.rounding.extraSmall / 2
|
bottomLeftRadius: Tokens.rounding.extraSmall / 2
|
||||||
color: root.bgColour
|
color: root.bgColour
|
||||||
|
|
@ -71,9 +72,9 @@ Slider {
|
||||||
|
|
||||||
implicitWidth: 4
|
implicitWidth: 4
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
const mult = parent.height <= 12 ? 3 : 1.2;
|
const t = CUtils.clamp((parent.height - 12) / 16, 0, 1);
|
||||||
const pressMult = parent.height <= 12 ? 4 : 1.5;
|
const lerp = (a, b) => a + (b - a) * t;
|
||||||
return parent.height * (mouse.pressed ? pressMult : mult);
|
return parent.height * (mouse.pressed ? lerp(3.5, 1.5) : lerp(3, 1.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
|
|
@ -103,7 +104,7 @@ Slider {
|
||||||
implicitWidth: root.filledWidth
|
implicitWidth: root.filledWidth
|
||||||
implicitHeight: root.height
|
implicitHeight: root.height
|
||||||
|
|
||||||
radius: Tokens.rounding.medium
|
radius: root.radius
|
||||||
topRightRadius: Tokens.rounding.extraSmall / 2
|
topRightRadius: Tokens.rounding.extraSmall / 2
|
||||||
bottomRightRadius: Tokens.rounding.extraSmall / 2
|
bottomRightRadius: Tokens.rounding.extraSmall / 2
|
||||||
color: root.fgColour
|
color: root.fgColour
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,14 @@ QtObject {
|
||||||
BluetoothPairing {}
|
BluetoothPairing {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Component {
|
||||||
|
// Audio
|
||||||
|
StackPage {
|
||||||
|
Component {
|
||||||
|
AudioPage {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
97
modules/nexus/common/AudioDeviceList.qml
Normal file
97
modules/nexus/common/AudioDeviceList.qml
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
|
||||||
|
ItemList {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var nodes: []
|
||||||
|
property int currentId: -1
|
||||||
|
property string iconName: "speaker"
|
||||||
|
|
||||||
|
signal selected(node: PwNode)
|
||||||
|
|
||||||
|
last: true
|
||||||
|
showList: true
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: [...root.nodes].sort((a, b) => (a.description || a.name || "").localeCompare(b.description || b.name || ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: device
|
||||||
|
|
||||||
|
required property PwNode modelData
|
||||||
|
required property int index
|
||||||
|
readonly property bool active: device.modelData?.id === root.currentId
|
||||||
|
|
||||||
|
anchors.left: root.list.contentItem.left
|
||||||
|
anchors.right: root.list.contentItem.right
|
||||||
|
implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
radius: Tokens.rounding.extraSmall
|
||||||
|
bottomLeftRadius: device.index === root.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||||
|
bottomRightRadius: device.index === root.list.count - 1 ? Tokens.rounding.extraLarge : radius
|
||||||
|
onClicked: root.selected(device.modelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: deviceLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
anchors.leftMargin: Tokens.padding.largeIncreased
|
||||||
|
anchors.rightMargin: Tokens.padding.largeIncreased
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
implicitHeight: devIcon.implicitHeight + Tokens.padding.small * 2
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: devIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.iconName
|
||||||
|
color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
|
||||||
|
font: Tokens.font.icon.medium
|
||||||
|
fill: device.active ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on fill {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: device.modelData?.description || device.modelData?.name || qsTr("Unknown")
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
text: "check"
|
||||||
|
color: Colours.palette.m3primary
|
||||||
|
font: Tokens.font.icon.medium
|
||||||
|
opacity: device.active ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
modules/nexus/common/SliderRow.qml
Normal file
89
modules/nexus/common/SliderRow.qml
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
|
||||||
|
ConnectedRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias icon: icon.text
|
||||||
|
property alias label: label.text
|
||||||
|
property alias valueLabel: valueLabel.text
|
||||||
|
property real value
|
||||||
|
|
||||||
|
signal moved(value: real)
|
||||||
|
|
||||||
|
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: rowLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Tokens.padding.large
|
||||||
|
anchors.leftMargin: Tokens.padding.largeIncreased
|
||||||
|
anchors.rightMargin: Tokens.padding.largeIncreased
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: Tokens.font.icon.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: label
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: valueLabel
|
||||||
|
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomMouseArea {
|
||||||
|
function onWheel(event: WheelEvent): void {
|
||||||
|
const step = GlobalConfig.services.audioIncrement;
|
||||||
|
if (event.angleDelta.y > 0)
|
||||||
|
root.moved(Math.min(1, root.value + step));
|
||||||
|
else if (event.angleDelta.y < 0)
|
||||||
|
root.moved(Math.max(0, root.value - step));
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: Tokens.padding.medium * 2
|
||||||
|
|
||||||
|
StyledSlider {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
implicitHeight: parent.implicitHeight
|
||||||
|
|
||||||
|
radius: Tokens.rounding.small
|
||||||
|
value: root.value
|
||||||
|
enabled: root.enabled
|
||||||
|
onInteraction: v => root.moved(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
modules/nexus/pages/AudioPage.qml
Normal file
135
modules/nexus/pages/AudioPage.qml
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.services
|
||||||
|
import qs.utils
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
import qs.modules.nexus.pages.audio
|
||||||
|
|
||||||
|
PageBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Audio")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: root.cappedWidth
|
||||||
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
|
|
||||||
|
// Output
|
||||||
|
SliderRow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
first: true
|
||||||
|
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
|
||||||
|
label: qsTr("Output")
|
||||||
|
valueLabel: Math.round(value * 100) + "%"
|
||||||
|
value: Audio.volume
|
||||||
|
enabled: !Audio.muted
|
||||||
|
onMoved: v => Audio.setVolume(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Muted")
|
||||||
|
checked: Audio.muted
|
||||||
|
onToggled: Audio.setStreamMuted(Audio.sink, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioDeviceList {
|
||||||
|
nodes: Audio.sinks
|
||||||
|
currentId: Audio.sink?.id ?? -1
|
||||||
|
iconName: "speaker"
|
||||||
|
placeholderIcon: "speaker"
|
||||||
|
placeholderText: qsTr("No output devices")
|
||||||
|
onSelected: node => Audio.setAudioSink(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input
|
||||||
|
SliderRow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Tokens.spacing.large - parent.spacing
|
||||||
|
first: true
|
||||||
|
icon: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
|
||||||
|
label: qsTr("Input")
|
||||||
|
valueLabel: Math.round(value * 100) + "%"
|
||||||
|
value: Audio.sourceVolume
|
||||||
|
enabled: !Audio.sourceMuted
|
||||||
|
onMoved: v => Audio.setSourceVolume(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Muted")
|
||||||
|
checked: Audio.sourceMuted
|
||||||
|
onToggled: Audio.setStreamMuted(Audio.source, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioDeviceList {
|
||||||
|
nodes: Audio.sources
|
||||||
|
currentId: Audio.source?.id ?? -1
|
||||||
|
iconName: "mic"
|
||||||
|
placeholderIcon: "mic_off"
|
||||||
|
placeholderText: qsTr("No input devices")
|
||||||
|
onSelected: node => Audio.setAudioSource(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-app volumes
|
||||||
|
ConnectedRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Tokens.spacing.large - parent.spacing
|
||||||
|
implicitHeight: appLayout.implicitHeight + appLayout.anchors.margins * 2
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
onClicked: root.nState.openSubPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: appLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
anchors.leftMargin: Tokens.padding.largeIncreased
|
||||||
|
anchors.rightMargin: Tokens.padding.largeIncreased
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
text: "tune"
|
||||||
|
font: Tokens.font.icon.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("App volumes")
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Audio.streams.length === 0 ? qsTr("No apps playing audio") : Audio.streams.length === 1 ? qsTr("1 app playing audio") : qsTr("%1 apps playing audio").arg(Audio.streams.length)
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
font: Tokens.font.label.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
animate: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
text: "chevron_right"
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: Tokens.font.icon.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue