import 'dart:convert'; 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'; import 'package:gomix_flutter/settings_tab.dart'; import 'package:gomix_flutter/error_page.dart'; import 'package:gomix_flutter/utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; void main() => runApp(const GoMixClient()); class GoMixClient extends StatelessWidget { const GoMixClient({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent), sliderTheme: const SliderThemeData( showValueIndicator: ShowValueIndicator.always, ), ), darkTheme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: Colors.greenAccent, brightness: Brightness.dark), sliderTheme: const SliderThemeData( showValueIndicator: ShowValueIndicator.always, ), ), themeMode: ThemeMode.system, home: const GoMixHome(), ); } } class GoMixHome extends StatefulWidget { const GoMixHome({super.key}); @override State createState() => _GoMixHomeState(); } class _GoMixHomeState extends State { int currentPageIndex = 0; bool wsConnected = false; mixer.MixerState mixerState = mixer.MixerState(inputs: [], outputs: []); WebSocketChannel? ws; late final SharedPreferences prefs; late final prefsFuture = SharedPreferences.getInstance().then((e) => {prefs = e, initWs()}); ValueNotifier connectionError = ValueNotifier(""); void initWs() { if (wsConnected) { ws?.sink.close(1000, "disconnect before initWs"); } setState(() { connectionError.value = ""; wsConnected = false; }); var connections = jsonDecode(prefs.getString("connections") ?? "{}") as Map; var currentConnection = prefs.getString("selectedConnection"); var wsUrl = (connections[currentConnection] ?? {"url": ""})["url"]; if (wsUrl == "") { setState(() { connectionError.value = "No GoMix connection available! Please add a connection in the server selector."; }); return; } ws = WebSocketChannel.connect( Uri.parse(wsUrl), ); ws?.ready.whenComplete(() => wsConnected = true); // Listen to incoming websocket messages ws?.stream.listen((message) { final data = jsonDecode(message) as Map; // Update complete port list if ((data["type"] == "response" && data["requestMethod"] == "listPorts") || data["type"] == "portListChange") { setState(() { mixerState = mixer.mixerStateFromJson(jsonEncode(data["responseData"])); }); } // Update a single port if (data["type"] == "portChange") { mixer.Port newPort = mixer.Port.fromJson(data["responseData"]); setState(() { mixer.Port? affectedPort = findPort(mixerState, data["responseData"]["UUID"]); if (affectedPort != null) { affectedPort.name = newPort.name; affectedPort.state = newPort.state; affectedPort.properties = newPort.properties; affectedPort.route = newPort.route; } }); } if (data["type"] == "error") { String errorMessage = ""; try { errorMessage = data["responseData"]["error"]; } catch (e) { errorMessage = "Unknown error"; } showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Server error'), content: Text(errorMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'OK'), child: const Text('OK'), ), ], ), ); } }, onDone: () { wsConnected = false; if (connectionError.value != "") return; Future.delayed(const Duration(seconds: 2), () { if (wsConnected) return; initWs(); }); }, onError: (err) { setState(() { connectionError.value = err.toString(); }); }); // Request the current port list ws?.sink.add(json.encode({"method": "listPorts"})); // Get notified about port changes ws?.sink.add(json.encode({"method": "enableUpdates"})); } void sendAction(Map data) { ws?.sink.add(json.encode(data)); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return FutureBuilder( future: prefsFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Scaffold( body: Center(child: CircularProgressIndicator())); } return Scaffold( bottomNavigationBar: NavigationBar( onDestinationSelected: (int index) { setState(() { currentPageIndex = index; }); }, selectedIndex: currentPageIndex, indicatorColor: theme.colorScheme.secondaryContainer, destinations: [ NavigationDestination( icon: Transform.rotate( angle: 90 * pi / 180, child: const Icon(Icons.tune_outlined)), label: 'Mixing', tooltip: "", ), const NavigationDestination( icon: Icon(Icons.settings_input_component_outlined), label: 'Ports', tooltip: "", ), const NavigationDestination( icon: Icon(Icons.settings_outlined), label: 'Settings', tooltip: "", ), ], ), appBar: AppBar( title: const Text('go-mix Audio Mixer'), centerTitle: true, leading: connectionError.value == "" ? (wsConnected ? IconButton( icon: const Icon(Icons.check_circle_outline), color: Colors.green, tooltip: 'Connected', onPressed: () {}, ) : IconButton( icon: const Icon(Icons.change_circle_outlined), color: Colors.orange, tooltip: 'Connecting', onPressed: () {}, )) : IconButton( icon: const Icon(Icons.remove_circle_outline_outlined), color: Colors.red, tooltip: 'Not connected', onPressed: () { initWs(); }, ), actions: [ Padding( padding: const EdgeInsets.all(8), child: IconButton( icon: const Icon(Icons.supervised_user_circle_outlined), tooltip: 'Select Server', onPressed: () => showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Select Instance'), content: ConnectionSelector( prefs: prefs, initWs: initWs, connections: (jsonDecode( prefs.getString("connections") ?? "{}")) as Map), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'Cancel'), child: const Text('Cancel'), ), ], ), ), ), ), ], ), body: Column(children: [ Expanded( child: FutureBuilder( future: ws?.ready, builder: (context, snapshot) { if (connectionError.value != "") { return const ErrorPage( message: "Could not connect to the GoMix instance!"); } if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } return [ MixingTab( mixerState: mixerState, sendAction: sendAction, ), PortsTab( mixerState: mixerState, sendAction: sendAction), const SettingsTab(), ][currentPageIndex]; }), ), ValueListenableBuilder( valueListenable: connectionError, builder: (ctx, value, child) { if (connectionError.value == "") { return const SizedBox(); } Future.delayed(const Duration(seconds: 0), () { showDialog( context: ctx, builder: (ctx) { return AlertDialog( title: const Text('Connection error'), content: Text(connectionError.value), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Ok'), ), ], ); }); }); 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, ); }); } }