From fb28b9361ad7dcf1f9d4801cee819e1c732be9e8 Mon Sep 17 00:00:00 2001 From: minie4 Date: Wed, 28 Aug 2024 23:14:16 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Implement=20port=20connection=20dia?= =?UTF-8?q?log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements: - Viewing connections (routes) - Creating and deleting connections - Changing volume of connectons - Muting / unmuting connections --- lib/mixing_card.dart | 52 +++++++++++- lib/mixing_tab.dart | 1 + lib/port_connection_card.dart | 135 ++++++++++++++++++++++++++++++++ lib/port_connection_dialog.dart | 132 +++++++++++++++++++++++++++++++ 4 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 lib/port_connection_card.dart create mode 100644 lib/port_connection_dialog.dart diff --git a/lib/mixing_card.dart b/lib/mixing_card.dart index f51bb0e..84584fb 100644 --- a/lib/mixing_card.dart +++ b/lib/mixing_card.dart @@ -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 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 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 { 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 { 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))) ], ), diff --git a/lib/mixing_tab.dart b/lib/mixing_tab.dart index 60c7bde..7127f07 100644 --- a/lib/mixing_tab.dart +++ b/lib/mixing_tab.dart @@ -27,6 +27,7 @@ class _MixingTabState extends State { port: p, sendAction: widget.sendAction, isOutput: isOutput, + mixerState: widget.mixerState, )) .toList(), ); diff --git a/lib/port_connection_card.dart b/lib/port_connection_card.dart new file mode 100644 index 0000000..7c55631 --- /dev/null +++ b/lib/port_connection_card.dart @@ -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 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 createState() => _PortConnectionCardState(); +} + +class _PortConnectionCardState extends State { + 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)), + ], + ) + ], + ); + } +} diff --git a/lib/port_connection_dialog.dart b/lib/port_connection_dialog.dart new file mode 100644 index 0000000..ad44db3 --- /dev/null +++ b/lib/port_connection_dialog.dart @@ -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 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( + context: context, + builder: (BuildContext context) => AlertDialog( + title: isOutput + ? Text("Inputs > ${port.name}") + : Text("${port.name} > Outputs"), + content: this, + ), + ); + } + + void showNewConnectionDialog(BuildContext context) { + showDialog( + 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 createState() => _PortConnectionState(); +} + +class _PortConnectionState extends State { + 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()) + + [ + const SizedBox(height: 10), + TextButton.icon( + onPressed: () { + widget.showNewConnectionDialog(context); + }, + icon: const Icon(Icons.add), + label: const Text("New connection")) + ], + ), + ); + } +}