diff --git a/lib/connection_editor.dart b/lib/connection_editor.dart new file mode 100644 index 0000000..165a7f7 --- /dev/null +++ b/lib/connection_editor.dart @@ -0,0 +1,84 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ConnectionEditor extends StatefulWidget { + final SharedPreferences prefs; + final Function updateData; + final String connectionName; + final dynamic connection; + const ConnectionEditor( + {super.key, + required this.prefs, + required this.updateData, + required this.connectionName, + required this.connection}); + + @override + State createState() => _ConnectionEditorState(); +} + +class _ConnectionEditorState extends State { + final wsRegex = RegExp(r'^(ws|wss):\/\/.*'); + final _formKey = GlobalKey(); + + String _connectionName = ""; + dynamic _connection; + bool _isValid = false; + + @override + void initState() { + super.initState(); + _connectionName = widget.connectionName; + _connection = widget.connection; + _isValid = + _connectionName != "" && wsRegex.hasMatch(_connection["url"] ?? ""); + widget.updateData(_connectionName, _connection, _isValid); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: min(MediaQuery.of(context).size.width, 400), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.always, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + initialValue: widget.connectionName, + onChanged: (value) { + _connectionName = value; + _isValid = _formKey.currentState!.validate(); + widget.updateData(_connectionName, _connection, _isValid); + }, + validator: (value) { + return value == "" ? "Name cannot be empty" : null; + }, + decoration: const InputDecoration( + labelText: "Name", border: OutlineInputBorder()), + ), + const SizedBox(height: 15), + TextFormField( + initialValue: widget.connection["url"], + onChanged: (value) { + _connection["url"] = value; + _isValid = _formKey.currentState!.validate(); + widget.updateData(_connectionName, _connection, _isValid); + }, + validator: (value) { + return wsRegex.hasMatch(value ?? "") + ? null + : "Must be: ws(s)://[host](:[port])/[path]"; + }, + decoration: const InputDecoration( + labelText: "Connection URL", border: OutlineInputBorder()), + ) + ], + ), + ), + ); + } +} diff --git a/lib/connection_selector.dart b/lib/connection_selector.dart new file mode 100644 index 0000000..223f3e3 --- /dev/null +++ b/lib/connection_selector.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:gomix_flutter/connection_editor.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ConnectionSelector extends StatefulWidget { + final SharedPreferences prefs; + final Function initWs; + final Map connections; + const ConnectionSelector( + {super.key, + required this.prefs, + required this.initWs, + required this.connections}); + + @override + State createState() => _ConnectionSelectorState(); +} + +class _ConnectionSelectorState extends State { + String _updateConnectionName = ""; + dynamic _updateConnection; + bool _updateValid = false; + + void updateData(String connectionName, dynamic connection, bool isValid) { + _updateConnectionName = connectionName; + _updateConnection = connection; + _updateValid = isValid; + } + + void openEditDialog(bool showDelete, void Function()? onDelete, + void Function()? onSubmit, String submitButtonText, String headerText) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(headerText), + content: ConnectionEditor( + connectionName: _updateConnectionName, + connection: _updateConnection, + prefs: widget.prefs, + updateData: updateData, + ), + actions: [ + if (showDelete) + TextButton( + onPressed: onDelete, + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: onSubmit, + child: Text(submitButtonText), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: min(MediaQuery.of(context).size.width, 400), + child: ListView( + shrinkWrap: true, + children: [ + ...widget.connections.keys.map( + (key) => ListTile( + contentPadding: const EdgeInsets.only(left: 5, right: 5), + title: Text(key), + trailing: IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () { + _updateValid = false; + _updateConnectionName = key; + _updateConnection = widget.connections[key]; + + openEditDialog(true, () { + setState(() { + widget.connections.removeWhere((k, _) => k == key); + widget.prefs.setString( + "connections", jsonEncode(widget.connections)); + }); + Navigator.pop(context, 'Cancel'); + }, () { + if (!_updateValid) return; + setState(() { + widget.connections.removeWhere((k, _) => k == key); + widget.connections[_updateConnectionName] = + _updateConnection; + widget.prefs.setString( + "connections", jsonEncode(widget.connections)); + }); + Navigator.pop(context, 'Save'); + }, "Save", "Edit Instance"); + }, + ), + onTap: () { + widget.prefs.setString("selectedConnection", key); + Navigator.pop(context, 'Select'); + widget.initWs(); + }, + ), + ), + const SizedBox(height: 5), + TextButton.icon( + onPressed: () { + _updateValid = false; + _updateConnection = {"url": ""}; + _updateConnectionName = + "Connection #${DateTime.now().millisecondsSinceEpoch.remainder(100000)}"; + openEditDialog(false, () {}, () { + if (!_updateValid) return; + setState(() { + widget.connections[_updateConnectionName] = + _updateConnection; + widget.prefs.setString( + "connections", jsonEncode(widget.connections)); + }); + Navigator.pop(context, 'Save'); + }, "Create", "Create Connection"); + }, + icon: const Icon(Icons.add), + label: const Text("New connection")) + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index ec66ced..76c5301 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:gomix_flutter/connection_selector.dart'; import 'package:gomix_flutter/mixer_state.dart' as mixer; import 'package:gomix_flutter/mixing_tab.dart'; import 'package:gomix_flutter/ports_tab.dart'; @@ -176,7 +177,12 @@ class _GoMixHomeState extends State { context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Select Instance'), - content: const Text("WIP"), + content: ConnectionSelector( + prefs: prefs, + initWs: initWs, + connections: (jsonDecode( + prefs.getString("connections") ?? "{}")) + as Map), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'Cancel'),