✨ Implement WebSocket API client
- List ports - Read and write mute state - Read and write volume
This commit is contained in:
32
lib/error_page.dart
Normal file
32
lib/error_page.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ErrorPage extends StatefulWidget {
|
||||||
|
final String message;
|
||||||
|
const ErrorPage({super.key, this.message = "Something went wrong!"});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ErrorPage> createState() => _ErrorPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorPageState extends State<ErrorPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.cloud_off_outlined,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
widget.message,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
259
lib/main.dart
259
lib/main.dart
@ -1,9 +1,14 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/mixing_tab.dart';
|
||||||
import 'package:gomix_flutter/ports_tab.dart';
|
import 'package:gomix_flutter/ports_tab.dart';
|
||||||
import 'package:gomix_flutter/settings_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());
|
void main() => runApp(const GoMixClient());
|
||||||
|
|
||||||
@ -39,80 +44,204 @@ class GoMixHome extends StatefulWidget {
|
|||||||
|
|
||||||
class _GoMixHomeState extends State<GoMixHome> {
|
class _GoMixHomeState extends State<GoMixHome> {
|
||||||
int currentPageIndex = 0;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
return Scaffold(
|
return FutureBuilder(
|
||||||
bottomNavigationBar: NavigationBar(
|
future: prefsFuture,
|
||||||
onDestinationSelected: (int index) {
|
builder: (context, snapshot) {
|
||||||
setState(() {
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
currentPageIndex = index;
|
return const Scaffold(
|
||||||
});
|
body: Center(child: CircularProgressIndicator()));
|
||||||
},
|
}
|
||||||
selectedIndex: currentPageIndex,
|
|
||||||
indicatorColor: theme.colorScheme.secondaryContainer,
|
return Scaffold(
|
||||||
destinations: <Widget>[
|
bottomNavigationBar: NavigationBar(
|
||||||
NavigationDestination(
|
onDestinationSelected: (int index) {
|
||||||
icon: Transform.rotate(
|
setState(() {
|
||||||
|
currentPageIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectedIndex: currentPageIndex,
|
||||||
|
indicatorColor: theme.colorScheme.secondaryContainer,
|
||||||
|
destinations: <Widget>[
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Transform.rotate(
|
||||||
angle: 90 * pi / 180,
|
angle: 90 * pi / 180,
|
||||||
child: const Icon(Icons.tune_outlined)),
|
child: const Icon(Icons.tune_outlined)),
|
||||||
label: 'Mixing',
|
label: 'Mixing',
|
||||||
tooltip: "",
|
tooltip: "",
|
||||||
),
|
),
|
||||||
const NavigationDestination(
|
const NavigationDestination(
|
||||||
icon: Icon(Icons.settings_input_component_outlined),
|
icon: Icon(Icons.settings_input_component_outlined),
|
||||||
label: 'Ports',
|
label: 'Ports',
|
||||||
tooltip: "",
|
tooltip: "",
|
||||||
),
|
),
|
||||||
const NavigationDestination(
|
const NavigationDestination(
|
||||||
icon: Icon(Icons.settings_outlined),
|
icon: Icon(Icons.settings_outlined),
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
tooltip: "",
|
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
appBar: AppBar(
|
||||||
],
|
title: const Text('go-mix Audio Mixer'),
|
||||||
),
|
centerTitle: true,
|
||||||
body: <Widget>[
|
leading: IconButton(
|
||||||
const MixingTab(),
|
icon: const Icon(Icons.check_circle_outline),
|
||||||
const PortsTab(),
|
color: theme.colorScheme.inversePrimary,
|
||||||
const SettingsTab(),
|
tooltip: 'Connected',
|
||||||
][currentPageIndex],
|
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();
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
108
lib/mixer_state.dart
Normal file
108
lib/mixer_state.dart
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Converted with: https://app.quicktype.io/?l=dart
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
MixerState mixerStateFromJson(String str) =>
|
||||||
|
MixerState.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String mixerStateToJson(MixerState data) => json.encode(data.toJson());
|
||||||
|
|
||||||
|
class MixerState {
|
||||||
|
List<Port> inputs;
|
||||||
|
List<Port> outputs;
|
||||||
|
|
||||||
|
MixerState({
|
||||||
|
required this.inputs,
|
||||||
|
required this.outputs,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MixerState.fromJson(Map<String, dynamic> json) => MixerState(
|
||||||
|
inputs: List<Port>.from(json["inputs"].map((x) => Port.fromJson(x))),
|
||||||
|
outputs: List<Port>.from(json["outputs"].map((x) => Port.fromJson(x))),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"inputs": List<dynamic>.from(inputs.map((x) => x.toJson())),
|
||||||
|
"outputs": List<dynamic>.from(outputs.map((x) => x.toJson())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Port {
|
||||||
|
String uuid;
|
||||||
|
String name;
|
||||||
|
Properties properties;
|
||||||
|
State state;
|
||||||
|
List<State> route;
|
||||||
|
|
||||||
|
Port({
|
||||||
|
required this.uuid,
|
||||||
|
required this.name,
|
||||||
|
required this.properties,
|
||||||
|
required this.state,
|
||||||
|
required this.route,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Port.fromJson(Map<String, dynamic> json) => Port(
|
||||||
|
uuid: json["UUID"],
|
||||||
|
name: json["name"],
|
||||||
|
properties: Properties.fromJson(json["properties"]),
|
||||||
|
state: State.fromJson(json["state"]),
|
||||||
|
route: List<State>.from(json["route"].map((x) => State.fromJson(x))),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"UUID": uuid,
|
||||||
|
"name": name,
|
||||||
|
"properties": properties.toJson(),
|
||||||
|
"state": state.toJson(),
|
||||||
|
"route": List<dynamic>.from(route.map((x) => x.toJson())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Properties {
|
||||||
|
String backend;
|
||||||
|
int channels;
|
||||||
|
|
||||||
|
Properties({
|
||||||
|
required this.backend,
|
||||||
|
required this.channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Properties.fromJson(Map<String, dynamic> json) => Properties(
|
||||||
|
backend: json["backend"],
|
||||||
|
channels: json["channels"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"backend": backend,
|
||||||
|
"channels": channels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class State {
|
||||||
|
String? toUuid;
|
||||||
|
bool mute;
|
||||||
|
double volume;
|
||||||
|
int balance;
|
||||||
|
|
||||||
|
State({
|
||||||
|
this.toUuid,
|
||||||
|
required this.mute,
|
||||||
|
required this.volume,
|
||||||
|
required this.balance,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory State.fromJson(Map<String, dynamic> json) => State(
|
||||||
|
toUuid: json["toUUID"],
|
||||||
|
mute: json["mute"],
|
||||||
|
volume: json["volume"]?.toDouble(),
|
||||||
|
balance: json["balance"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"toUUID": toUuid,
|
||||||
|
"mute": mute,
|
||||||
|
"volume": volume,
|
||||||
|
"balance": balance,
|
||||||
|
};
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gomix_flutter/mixer_state.dart' as mixer;
|
||||||
|
|
||||||
class MixingCard extends StatefulWidget {
|
class MixingCard extends StatefulWidget {
|
||||||
final String name;
|
final mixer.Port port;
|
||||||
const MixingCard({super.key, this.name = "Unknown"});
|
final Function sendAction;
|
||||||
|
const MixingCard({super.key, required this.port, required this.sendAction});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MixingCard> createState() => _MixingCardState();
|
State<MixingCard> createState() => _MixingCardState();
|
||||||
@ -10,6 +12,21 @@ class MixingCard extends StatefulWidget {
|
|||||||
|
|
||||||
class _MixingCardState extends State<MixingCard> {
|
class _MixingCardState extends State<MixingCard> {
|
||||||
double _sliderValue = 0;
|
double _sliderValue = 0;
|
||||||
|
int _debounceTimer = 0;
|
||||||
|
bool _sliderActive = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_sliderValue = widget.port.state.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MixingCard oldWidget) {
|
||||||
|
if (_sliderActive) return;
|
||||||
|
_sliderValue = widget.port.state.volume;
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -26,34 +43,75 @@ class _MixingCardState extends State<MixingCard> {
|
|||||||
padding: const EdgeInsets.only(left: 15, right: 15, top: 15),
|
padding: const EdgeInsets.only(left: 15, right: 15, top: 15),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(widget.name, style: labelStyle)),
|
Expanded(child: Text(widget.port.name, style: labelStyle)),
|
||||||
IconButton.filledTonal(
|
SizedBox(
|
||||||
isSelected: true,
|
height: 40,
|
||||||
onPressed: () {},
|
width: 40,
|
||||||
icon: const Icon(Icons.mic_off_outlined)),
|
child: IconButton.filledTonal(
|
||||||
|
iconSize: 20,
|
||||||
|
isSelected: widget.port.state.mute,
|
||||||
|
onPressed: () {
|
||||||
|
widget.sendAction({
|
||||||
|
"method": "setPortState",
|
||||||
|
"UUID": widget.port.uuid,
|
||||||
|
"stateData": {"mute": !widget.port.state.mute}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: widget.port.state.mute
|
||||||
|
? const Icon(Icons.mic_off_outlined)
|
||||||
|
: const Icon(Icons.mic_none_outlined))),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
IconButton.outlined(
|
SizedBox(
|
||||||
isSelected: false,
|
height: 40,
|
||||||
onPressed: () {},
|
width: 40,
|
||||||
icon: const Icon(Icons.unfold_more))
|
child: IconButton.outlined(
|
||||||
|
iconSize: 20,
|
||||||
|
isSelected: false,
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.unfold_more)))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Transform.translate(
|
SizedBox(
|
||||||
offset: Offset.fromDirection(0, 0),
|
width: MediaQuery.of(context).size.width + 180,
|
||||||
child: SizedBox(
|
child: Slider(
|
||||||
width: MediaQuery.of(context).size.width + 180,
|
value: _sliderValue,
|
||||||
child: Slider(
|
min: 0,
|
||||||
value: _sliderValue,
|
max: 4,
|
||||||
secondaryTrackValue: 1,
|
label: _sliderValue.toStringAsFixed(2),
|
||||||
min: 0,
|
onChangeStart: ((val) {
|
||||||
max: 4,
|
_sliderActive = true;
|
||||||
label: _sliderValue.toStringAsFixed(2),
|
}),
|
||||||
onChanged: (val) {
|
onChangeEnd: ((val) {
|
||||||
setState(() {
|
// Send the current state to make sure we
|
||||||
_sliderValue = val;
|
// don't miss the last value due to the debounce
|
||||||
});
|
widget.sendAction({
|
||||||
}),
|
"method": "setPortState",
|
||||||
|
"UUID": widget.port.uuid,
|
||||||
|
"stateData": {"volume": val}
|
||||||
|
});
|
||||||
|
// Make sure the slider value is still correct
|
||||||
|
// after suppressing updates
|
||||||
|
_sliderActive = false;
|
||||||
|
setState(() {
|
||||||
|
_sliderValue = widget.port.state.volume;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
_sliderValue = val;
|
||||||
|
});
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - _debounceTimer <
|
||||||
|
30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_debounceTimer = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
widget.sendAction({
|
||||||
|
"method": "setPortState",
|
||||||
|
"UUID": widget.port.uuid,
|
||||||
|
"stateData": {"volume": val}
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,19 +1,40 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gomix_flutter/mixing_card.dart';
|
import 'package:gomix_flutter/mixing_card.dart';
|
||||||
|
import 'package:gomix_flutter/mixer_state.dart' as mixer;
|
||||||
|
|
||||||
class MixingTab extends StatefulWidget {
|
class MixingTab extends StatefulWidget {
|
||||||
const MixingTab({super.key});
|
final mixer.MixerState mixerState;
|
||||||
|
final Function sendAction;
|
||||||
|
const MixingTab(
|
||||||
|
{super.key, required this.mixerState, required this.sendAction});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MixingTab> createState() => _MixingTabState();
|
State<MixingTab> createState() => _MixingTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MixingTabState extends State<MixingTab> {
|
class _MixingTabState extends State<MixingTab> {
|
||||||
|
Widget buildCardGrid(
|
||||||
|
List<mixer.Port> from, int cols, double cardGridAspectRatio) {
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
childAspectRatio: cardGridAspectRatio,
|
||||||
|
crossAxisCount: cols,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
children: from
|
||||||
|
.map((p) => MixingCard(port: p, sendAction: widget.sendAction))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double screenWidth = MediaQuery.of(context).size.width;
|
double screenWidth = MediaQuery.of(context).size.width;
|
||||||
int cols = 1 + (screenWidth ~/ 550);
|
int cols = 1 + (screenWidth ~/ 550);
|
||||||
int cardHeight = 110;
|
int cardHeight = 110;
|
||||||
|
double cardGridAspectRatio =
|
||||||
|
(screenWidth - 32 - (10 * (cols - 1))) / cardHeight / cols;
|
||||||
|
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@ -24,17 +45,13 @@ class _MixingTabState extends State<MixingTab> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Inputs", style: Theme.of(context).textTheme.titleLarge),
|
Text("Inputs", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
GridView.count(
|
buildCardGrid(
|
||||||
shrinkWrap: true,
|
widget.mixerState.inputs, cols, cardGridAspectRatio),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
const SizedBox(height: 20),
|
||||||
childAspectRatio:
|
Text("Outputs", style: Theme.of(context).textTheme.titleLarge),
|
||||||
(screenWidth - 32 - (10 * (cols - 1))) / cardHeight / cols,
|
const SizedBox(height: 10),
|
||||||
crossAxisCount: cols,
|
buildCardGrid(
|
||||||
mainAxisSpacing: 10,
|
widget.mixerState.outputs, cols, cardGridAspectRatio),
|
||||||
crossAxisSpacing: 10,
|
|
||||||
children: List<Widget>.generate(
|
|
||||||
6, (i) => MixingCard(name: "Port ${i.toString()}")),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
166
pubspec.lock
166
pubspec.lock
@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -49,6 +57,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -67,6 +91,11 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -131,6 +160,102 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.5"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -184,6 +309,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.1"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -200,5 +333,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "13.0.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.4"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.0"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
|
flutter: ">=3.19.0"
|
||||||
|
@ -9,6 +9,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
shared_preferences: ^2.2.2
|
||||||
|
web_socket_channel: ^2.4.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user