✨ 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 '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();
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
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:gomix_flutter/mixer_state.dart' as mixer;
|
||||
|
||||
class MixingCard extends StatefulWidget {
|
||||
final String name;
|
||||
const MixingCard({super.key, this.name = "Unknown"});
|
||||
final mixer.Port port;
|
||||
final Function sendAction;
|
||||
const MixingCard({super.key, required this.port, required this.sendAction});
|
||||
|
||||
@override
|
||||
State<MixingCard> createState() => _MixingCardState();
|
||||
@ -10,6 +12,21 @@ class MixingCard extends StatefulWidget {
|
||||
|
||||
class _MixingCardState extends State<MixingCard> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@ -26,34 +43,75 @@ class _MixingCardState extends State<MixingCard> {
|
||||
padding: const EdgeInsets.only(left: 15, right: 15, top: 15),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(widget.name, style: labelStyle)),
|
||||
IconButton.filledTonal(
|
||||
isSelected: true,
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.mic_off_outlined)),
|
||||
Expanded(child: Text(widget.port.name, style: labelStyle)),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
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),
|
||||
IconButton.outlined(
|
||||
isSelected: false,
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.unfold_more))
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: IconButton.outlined(
|
||||
iconSize: 20,
|
||||
isSelected: false,
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.unfold_more)))
|
||||
],
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset.fromDirection(0, 0),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width + 180,
|
||||
child: Slider(
|
||||
value: _sliderValue,
|
||||
secondaryTrackValue: 1,
|
||||
min: 0,
|
||||
max: 4,
|
||||
label: _sliderValue.toStringAsFixed(2),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_sliderValue = val;
|
||||
});
|
||||
}),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width + 180,
|
||||
child: Slider(
|
||||
value: _sliderValue,
|
||||
min: 0,
|
||||
max: 4,
|
||||
label: _sliderValue.toStringAsFixed(2),
|
||||
onChangeStart: ((val) {
|
||||
_sliderActive = true;
|
||||
}),
|
||||
onChangeEnd: ((val) {
|
||||
// Send the current state to make sure we
|
||||
// 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:gomix_flutter/mixing_card.dart';
|
||||
import 'package:gomix_flutter/mixer_state.dart' as mixer;
|
||||
|
||||
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
|
||||
State<MixingTab> createState() => _MixingTabState();
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
int cols = 1 + (screenWidth ~/ 550);
|
||||
int cardHeight = 110;
|
||||
double cardGridAspectRatio =
|
||||
(screenWidth - 32 - (10 * (cols - 1))) / cardHeight / cols;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: SingleChildScrollView(
|
||||
@ -24,17 +45,13 @@ class _MixingTabState extends State<MixingTab> {
|
||||
children: <Widget>[
|
||||
Text("Inputs", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 10),
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio:
|
||||
(screenWidth - 32 - (10 * (cols - 1))) / cardHeight / cols,
|
||||
crossAxisCount: cols,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
children: List<Widget>.generate(
|
||||
6, (i) => MixingCard(name: "Port ${i.toString()}")),
|
||||
),
|
||||
buildCardGrid(
|
||||
widget.mixerState.inputs, cols, cardGridAspectRatio),
|
||||
const SizedBox(height: 20),
|
||||
Text("Outputs", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 10),
|
||||
buildCardGrid(
|
||||
widget.mixerState.outputs, cols, cardGridAspectRatio),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
|
||||
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"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -49,6 +57,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -67,6 +91,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -131,6 +160,102 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -184,6 +309,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -200,5 +333,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
@ -9,6 +9,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
shared_preferences: ^2.2.2
|
||||
web_socket_channel: ^2.4.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user