✨ Implement WebSocket API client
- List ports - Read and write mute state - Read and write volume
This commit is contained in:
259
lib/main.dart
259
lib/main.dart
@ -1,9 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.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:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
void main() => runApp(const GoMixClient());
|
||||
|
||||
@ -39,80 +44,204 @@ class GoMixHome extends StatefulWidget {
|
||||
|
||||
class _GoMixHomeState extends State<GoMixHome> {
|
||||
int currentPageIndex = 0;
|
||||
mixer.MixerState mixerState = mixer.MixerState(inputs: [], outputs: []);
|
||||
|
||||
WebSocketChannel? ws;
|
||||
late final SharedPreferences prefs;
|
||||
late final prefsFuture =
|
||||
SharedPreferences.getInstance().then((e) => {prefs = e, initWs()});
|
||||
|
||||
ValueNotifier<String> connectionError = ValueNotifier("");
|
||||
|
||||
void initWs() {
|
||||
setState(() {
|
||||
connectionError.value = "";
|
||||
});
|
||||
|
||||
var connections = jsonDecode(prefs.getString("connections") ?? "{}")
|
||||
as Map<String, dynamic>;
|
||||
var currentConnection = prefs.getString("selectedConnection");
|
||||
var wsUrl = (connections[currentConnection] ?? {"url": ""})["url"];
|
||||
|
||||
if (wsUrl == "") {
|
||||
connectionError.value =
|
||||
"No GoMix connection available! Please add a connection in the server selector.";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ws = WebSocketChannel.connect(
|
||||
Uri.parse(wsUrl),
|
||||
);
|
||||
|
||||
// Listen to incoming websocket messages
|
||||
ws?.stream.listen((message) {
|
||||
final data = jsonDecode(message) as Map<String, dynamic>;
|
||||
|
||||
// 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") {
|
||||
int index = mixerState.inputs.indexWhere(
|
||||
(element) => element.uuid == data["responseData"]["UUID"]);
|
||||
int index2 = mixerState.outputs.indexWhere(
|
||||
(element) => element.uuid == data["responseData"]["UUID"]);
|
||||
|
||||
mixer.Port newPort = mixer.Port.fromJson(data["responseData"]);
|
||||
setState(() {
|
||||
if (index >= 0) {
|
||||
mixerState.inputs[index] = newPort;
|
||||
} else if (index2 >= 0) {
|
||||
mixerState.outputs[index2] = newPort;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, onError: (err) {
|
||||
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<String, Object> data) {
|
||||
ws?.sink.add(json.encode(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (int index) {
|
||||
setState(() {
|
||||
currentPageIndex = index;
|
||||
});
|
||||
},
|
||||
selectedIndex: currentPageIndex,
|
||||
indicatorColor: theme.colorScheme.secondaryContainer,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Transform.rotate(
|
||||
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: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Transform.rotate(
|
||||
angle: 90 * pi / 180,
|
||||
child: const Icon(Icons.tune_outlined)),
|
||||
label: 'Mixing',
|
||||
label: 'Mixing',
|
||||
tooltip: "",
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_input_component_outlined),
|
||||
label: 'Ports',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_input_component_outlined),
|
||||
label: 'Ports',
|
||||
tooltip: "",
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
label: 'Settings',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
label: 'Settings',
|
||||
tooltip: "",
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: const Text('go-mix Audio Mixer'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
color: theme.colorScheme.inversePrimary,
|
||||
tooltip: 'Connected',
|
||||
onPressed: () {},
|
||||
),
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.supervised_user_circle_outlined),
|
||||
tooltip: 'Select Server',
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Next page'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(
|
||||
'This is the next page',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: <Widget>[
|
||||
const MixingTab(),
|
||||
const PortsTab(),
|
||||
const SettingsTab(),
|
||||
][currentPageIndex],
|
||||
);
|
||||
appBar: AppBar(
|
||||
title: const Text('go-mix Audio Mixer'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
color: theme.colorScheme.inversePrimary,
|
||||
tooltip: 'Connected',
|
||||
onPressed: () {},
|
||||
),
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.supervised_user_circle_outlined),
|
||||
tooltip: 'Select Server',
|
||||
onPressed: () => showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Select Instance'),
|
||||
content: const Text("WIP"),
|
||||
actions: <Widget>[
|
||||
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 <Widget>[
|
||||
MixingTab(
|
||||
mixerState: mixerState,
|
||||
sendAction: sendAction,
|
||||
),
|
||||
const PortsTab(),
|
||||
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();
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user