diff --git a/lib/edit_port_dialog.dart b/lib/edit_port_dialog.dart new file mode 100644 index 0000000..2d2de4f --- /dev/null +++ b/lib/edit_port_dialog.dart @@ -0,0 +1,103 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:gomix_flutter/mixer_state.dart'; +import 'package:gomix_flutter/select_button.dart'; + +const List> availableBackends = [ + DropdownMenuEntry(value: "jack", label: "Jack") +]; + +const portTypes = ["Input", "Output"]; + +class CreatePortDialog { + String portType = portTypes[0].toLowerCase(); + final GlobalKey _portDialogForm = GlobalKey(); + + void show( + BuildContext context, + final void Function(Map data) sendAction, + Port editPortRef, + bool isNewPort) { + Port editPort = Port.fromJson(editPortRef.toJson()); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: isNewPort ? const Text("Create Port") : const Text("Edit Port"), + content: + StatefulBuilder(builder: (BuildContext ctx, StateSetter setState) { + return SizedBox( + width: min(MediaQuery.of(context).size.width, 400), + child: Form( + key: _portDialogForm, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: "Name", border: OutlineInputBorder()), + onSaved: (String? value) { + editPort.name = value!; + }, + initialValue: editPort.name, + ), + ] + + (isNewPort + ? [ + const SizedBox(height: 15), + SelectButton( + options: portTypes, + defaultIndex: 0, + allowNull: false, + onChange: (int selection) { + portType = portTypes[selection].toLowerCase(); + }, + ), + const SizedBox(height: 30), + DropdownMenu( + initialSelection: availableBackends[0].value, + onSelected: (String? value) { + editPort.properties.backend = value!; + }, + label: const Text("Backend"), + dropdownMenuEntries: availableBackends, + ), + ] + : [])), + ), + ); + }), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + _portDialogForm.currentState?.save(); + if (isNewPort) { + sendAction({ + "method": "createPort", + "portData": { + "name": editPort.name, + "backend": editPort.properties.backend, + "channels": 2, + "type": portType + } + }); + } else { + sendAction({ + "method": "editPort", + "UUID": editPort.uuid, + "portProperties": {"name": editPort.name} + }); + } + Navigator.pop(context, 'Update'); + }, + child: isNewPort ? const Text("Create") : const Text("Update"), + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index fb6d154..84eaaab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:gomix_flutter/connection_selector.dart'; +import 'package:gomix_flutter/edit_port_dialog.dart'; import 'package:gomix_flutter/mixer_state.dart' as mixer; import 'package:gomix_flutter/mixing_tab.dart'; import 'package:gomix_flutter/ports_tab.dart'; @@ -243,7 +244,8 @@ class _GoMixHomeState extends State { mixerState: mixerState, sendAction: sendAction, ), - const PortsTab(), + PortsTab( + mixerState: mixerState, sendAction: sendAction), const SettingsTab(), ][currentPageIndex]; }), @@ -275,6 +277,25 @@ class _GoMixHomeState extends State { return const SizedBox(); }), ]), + floatingActionButton: currentPageIndex == 1 + ? FloatingActionButton( + tooltip: "Create Port", + child: const Icon(Icons.add), + onPressed: () { + mixer.Port newPort = mixer.Port( + uuid: "", + name: "", + properties: + mixer.Properties(backend: "jack", channels: 2), + state: + mixer.State(mute: false, volume: 1, balance: 1), + route: []); + + CreatePortDialog() + .show(context, sendAction, newPort, true); + }, + ) + : null, ); }); } diff --git a/lib/ports_tab.dart b/lib/ports_tab.dart index 2471ae3..b8c276d 100644 --- a/lib/ports_tab.dart +++ b/lib/ports_tab.dart @@ -1,35 +1,95 @@ import 'package:flutter/material.dart'; +import 'package:gomix_flutter/edit_port_dialog.dart'; +import 'package:gomix_flutter/mixer_state.dart' as mixer; + +enum MenuAction { edit, delete } class PortsTab extends StatefulWidget { - const PortsTab({super.key}); + final mixer.MixerState mixerState; + final void Function(Map data) sendAction; + const PortsTab( + {super.key, required this.mixerState, required this.sendAction}); @override State createState() => _PortsTabState(); } class _PortsTabState extends State { - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [ - Card( - child: ListTile( - leading: Icon(Icons.mic_none), - title: Text('Port 1'), - subtitle: Text('...'), - ), + void onEditPort(mixer.Port port) { + CreatePortDialog().show(context, widget.sendAction, port, false); + } + + void onDeletePort(mixer.Port port) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Delete Port"), + content: Text("Are you sure you want to delete ${port.name}?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), ), - Card( - child: ListTile( - leading: Icon(Icons.speaker_outlined), - title: Text('Port 2'), - subtitle: Text('...'), - ), + TextButton( + onPressed: () { + widget.sendAction({"method": "deletePort", "uuid": port.uuid}); + Navigator.pop(context, 'Delete'); + }, + child: const Text("Delete"), ), ], ), ); } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [...widget.mixerState.inputs, ...widget.mixerState.outputs] + .indexed + .map( + (elem) => Card( + child: ListTile( + leading: (elem.$1 >= widget.mixerState.inputs.length) + ? const Icon(Icons.speaker_outlined) + : const Icon(Icons.mic_none_outlined), + title: Text(elem.$2.name), + subtitle: Text('Backend: ${elem.$2.properties.backend}'), + trailing: PopupMenuButton( + tooltip: "More Options", + onSelected: (MenuAction item) { + switch (item) { + case MenuAction.edit: + onEditPort(elem.$2); + case MenuAction.delete: + onDeletePort(elem.$2); + } + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: MenuAction.edit, + child: ListTile( + leading: Icon(Icons.mode_edit), + title: Text('Edit'), + ), + ), + const PopupMenuItem( + value: MenuAction.delete, + child: ListTile( + leading: Icon(Icons.delete), + title: Text('Delete'), + ), + ), + ], + ), + ), + ), + ) + .toList(), + ), + ); + } } diff --git a/lib/select_button.dart b/lib/select_button.dart new file mode 100644 index 0000000..ff4320c --- /dev/null +++ b/lib/select_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class SelectButton extends StatefulWidget { + final List options; + final int defaultIndex; + final bool allowNull; + final bool isDisabled; + final void Function(int selection) onChange; + const SelectButton( + {super.key, + required this.options, + this.defaultIndex = 0, + this.allowNull = true, + this.isDisabled = false, + required this.onChange}); + + @override + State createState() => _SelectButtonState(); +} + +class _SelectButtonState extends State { + int _selection = 0; + + @override + void initState() { + super.initState(); + _selection = widget.defaultIndex; + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 5.0, + children: List.generate( + widget.options.length, + (int index) { + return ChoiceChip( + label: Text(widget.options[index]), + selected: _selection == index, + onSelected: (bool selected) { + if (widget.isDisabled) return; + if (!selected && !widget.allowNull) return; + + setState(() { + _selection = selected ? index : -1; + widget.onChange(_selection); + }); + }, + ); + }, + ).toList(), + ); + } +}