Implement port connection dialog

Implements:
- Viewing connections (routes)
- Creating and deleting connections
- Changing volume of connectons
- Muting / unmuting connections
This commit is contained in:
2024-08-28 23:14:16 +02:00
parent 340d6033ef
commit fb28b9361a
4 changed files with 318 additions and 2 deletions

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:gomix_flutter/mixer_state.dart' as mixer;
import 'package:gomix_flutter/port_connection_dialog.dart';
import 'package:gomix_flutter/utils.dart';
class MixingCard extends StatefulWidget {
final mixer.Port port;
final Function sendAction;
final mixer.MixerState mixerState;
final Function(Map<String, Object> data) sendAction;
final bool isOutput;
const MixingCard(
{super.key,
required this.port,
required this.mixerState,
required this.sendAction,
this.isOutput = false});
@ -15,20 +19,55 @@ class MixingCard extends StatefulWidget {
State<MixingCard> createState() => _MixingCardState();
}
class DialogDataModel extends ChangeNotifier {
String? portUUID;
late mixer.MixerState mixerState;
late mixer.Port port;
void updatePort(bool force) {
if (portUUID == null) {
return;
}
// Only update port reference if it is actually outdated
if (force ||
(!mixerState.inputs.contains(port) &&
!mixerState.outputs.contains(port))) {
mixer.Port? newPort = findPort(mixerState, portUUID!);
if (newPort != null) port = newPort;
}
}
void setPortUUID(String uuid) {
portUUID = uuid;
updatePort(true);
}
void setMixerState(mixer.MixerState state) {
mixerState = state;
updatePort(false);
notifyListeners();
}
}
class _MixingCardState extends State<MixingCard> {
double _sliderValue = 0;
int _debounceTimer = 0;
bool _sliderActive = false;
DialogDataModel dataModel = DialogDataModel();
@override
void initState() {
super.initState();
dataModel.setMixerState(widget.mixerState);
_sliderValue = widget.port.state.volume;
}
@override
void didUpdateWidget(MixingCard oldWidget) {
if (_sliderActive) return;
dataModel.setMixerState(widget.mixerState);
_sliderValue = widget.port.state.volume;
super.didUpdateWidget(oldWidget);
}
@ -78,7 +117,16 @@ class _MixingCardState extends State<MixingCard> {
child: IconButton.outlined(
iconSize: 20,
isSelected: false,
onPressed: () {},
onPressed: () {
dataModel.setPortUUID(widget.port.uuid);
PortConnectionDialog(
sendAction: widget.sendAction,
port: widget.port,
isOutput: widget.isOutput,
dataModel: dataModel,
mixerState: widget.mixerState)
.show(context);
},
icon: const Icon(Icons.unfold_more)))
],
),

View File

@ -27,6 +27,7 @@ class _MixingTabState extends State<MixingTab> {
port: p,
sendAction: widget.sendAction,
isOutput: isOutput,
mixerState: widget.mixerState,
))
.toList(),
);

View File

@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:gomix_flutter/mixer_state.dart' as mixer;
class PortConnectionCard extends StatefulWidget {
final mixer.Port sourcePort;
final mixer.State connection;
final mixer.MixerState mixerState;
final Function(Map<String, Object> data) sendAction;
final bool isOutput;
const PortConnectionCard(
{super.key,
required this.sourcePort,
required this.connection,
required this.mixerState,
required this.sendAction,
required this.isOutput});
@override
State<PortConnectionCard> createState() => _PortConnectionCardState();
}
class _PortConnectionCardState extends State<PortConnectionCard> {
double _sliderValue = 0;
int _debounceTimer = 0;
bool _sliderActive = false;
String toUUID = "";
String portName = "";
void updatePortData() {
toUUID = widget.connection.toUuid ?? "";
mixer.Port toPort = widget.mixerState.outputs.firstWhere(
(el) => el.uuid == toUUID,
orElse: () => mixer.Port(
name: "Invalid Port",
properties: mixer.Properties(backend: "N/A", channels: 0),
state: mixer.State(balance: 1, mute: true, volume: 0),
uuid: toUUID,
route: []));
portName = widget.isOutput ? widget.sourcePort.name : toPort.name;
}
@override
void initState() {
_sliderValue = widget.connection.volume;
updatePortData();
super.initState();
}
@override
void didUpdateWidget(PortConnectionCard oldWidget) {
if (_sliderActive) return;
_sliderValue = widget.connection.volume;
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(portName),
Row(
children: [
Expanded(
child: Slider(
min: 0,
max: 4,
value: _sliderValue,
label: _sliderValue.toStringAsFixed(2),
onChangeStart: ((val) {
_sliderActive = true;
}),
onChangeEnd: ((val) {
// Send the current state to make sure we
// don't miss the last value due to the debounce
widget.sendAction({
"method": "setRouteState",
"UUID": widget.sourcePort.uuid,
"toUUID": widget.connection.toUuid ?? "",
"routeStateData": {"volume": val}
});
// Make sure the slider value is still correct
// after suppressing updates
_sliderActive = false;
setState(() {
_sliderValue = widget.connection.volume;
});
}),
onChanged: (val) {
setState(() {
_sliderValue = val;
});
if (DateTime.now().millisecondsSinceEpoch -
_debounceTimer <
30) {
return;
}
_debounceTimer = DateTime.now().millisecondsSinceEpoch;
widget.sendAction({
"method": "setRouteState",
"UUID": widget.sourcePort.uuid,
"toUUID": widget.connection.toUuid ?? "",
"routeStateData": {"volume": val}
});
})),
IconButton.filledTonal(
onPressed: () {
widget.sendAction({
"method": "setRouteState",
"UUID": widget.sourcePort.uuid,
"toUUID": widget.connection.toUuid ?? "",
"routeStateData": {"mute": !widget.connection.mute}
});
},
iconSize: 20,
icon: widget.connection.mute
? const Icon(Icons.volume_off_outlined)
: const Icon(Icons.volume_up_outlined)),
const SizedBox(width: 5),
IconButton.filled(
onPressed: () {
widget.sendAction({
"method": "deleteRoute",
"UUID": widget.sourcePort.uuid,
"toUUID": widget.connection.toUuid ?? ""
});
},
iconSize: 20,
icon: const Icon(Icons.delete)),
],
)
],
);
}
}

View File

@ -0,0 +1,132 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gomix_flutter/mixer_state.dart' as mixer;
import 'package:gomix_flutter/mixing_card.dart';
import 'package:gomix_flutter/port_connection_card.dart';
import 'package:gomix_flutter/ports_tab.dart';
class PortConnectionDialog extends StatefulWidget {
final Function(Map<String, Object> data) sendAction;
final mixer.Port port;
final bool isOutput;
final DialogDataModel dataModel;
final mixer.MixerState mixerState;
const PortConnectionDialog(
{super.key,
required this.sendAction,
required this.port,
required this.dataModel,
this.isOutput = false,
required this.mixerState});
void show(BuildContext context) {
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: isOutput
? Text("Inputs > ${port.name}")
: Text("${port.name} > Outputs"),
content: this,
),
);
}
void showNewConnectionDialog(BuildContext context) {
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text("New connection"),
content: PortsTab(
mixerState: mixerState,
sendAction: sendAction,
// Can only select outputs if source port is an input and nice versa
filter: (_, isOutput) => this.isOutput != isOutput,
selectionCallback: (destination) {
Navigator.pop(context);
sendAction({
"method": "createRoute",
"UUID": isOutput ? destination.uuid : port.uuid,
"routeData": {"to": isOutput ? port.uuid : destination.uuid}
});
},
),
),
);
}
@override
State<PortConnectionDialog> createState() => _PortConnectionState();
}
class _PortConnectionState extends State<PortConnectionDialog> {
List<(mixer.Port, mixer.State)> connections = [];
List<(mixer.Port, mixer.State)> getConnections() {
List<(mixer.Port, mixer.State)> connections = [];
if (widget.isOutput) {
for (mixer.Port input in widget.dataModel.mixerState.inputs) {
for (mixer.State connection in input.route) {
String toUUID = connection.toUuid ?? "";
if (toUUID == widget.port.uuid) connections.add((input, connection));
}
}
} else {
connections = widget.dataModel.port.route
.map((route) => (widget.port, route))
.toList();
}
return connections;
}
var dataModelListener = () {};
@override
void initState() {
super.initState();
connections = getConnections();
dataModelListener = () {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
connections = getConnections();
});
});
};
widget.dataModel.addListener(dataModelListener);
}
@override
void dispose() {
widget.dataModel.removeListener(dataModelListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: min(MediaQuery.of(context).size.width, 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: (connections
.map((elem) => PortConnectionCard(
sourcePort: elem.$1,
connection: elem.$2,
isOutput: widget.isOutput,
mixerState: widget.mixerState,
sendAction: widget.sendAction) as Widget)
.toList()) +
<Widget>[
const SizedBox(height: 10),
TextButton.icon(
onPressed: () {
widget.showNewConnectionDialog(context);
},
icon: const Icon(Icons.add),
label: const Text("New connection"))
],
),
);
}
}