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 real waveFrequency: 6
|
||||
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 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))
|
||||
opacity: Math.min(width, 12) / 12
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
radius: root.radius
|
||||
topLeftRadius: Tokens.rounding.extraSmall / 2
|
||||
bottomLeftRadius: Tokens.rounding.extraSmall / 2
|
||||
color: root.bgColour
|
||||
|
|
@ -71,9 +72,9 @@ Slider {
|
|||
|
||||
implicitWidth: 4
|
||||
implicitHeight: {
|
||||
const mult = parent.height <= 12 ? 3 : 1.2;
|
||||
const pressMult = parent.height <= 12 ? 4 : 1.5;
|
||||
return parent.height * (mouse.pressed ? pressMult : mult);
|
||||
const t = CUtils.clamp((parent.height - 12) / 16, 0, 1);
|
||||
const lerp = (a, b) => a + (b - a) * t;
|
||||
return parent.height * (mouse.pressed ? lerp(3.5, 1.5) : lerp(3, 1.2));
|
||||
}
|
||||
|
||||
radius: Tokens.rounding.full
|
||||
|
|
@ -103,7 +104,7 @@ Slider {
|
|||
implicitWidth: root.filledWidth
|
||||
implicitHeight: root.height
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
radius: root.radius
|
||||
topRightRadius: Tokens.rounding.extraSmall / 2
|
||||
bottomRightRadius: Tokens.rounding.extraSmall / 2
|
||||
color: root.fgColour
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ QtObject {
|
|||
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