diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3bce123..869ed65 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ + android:icon="@mipmap/launcher_icon"> loadToken() async { final prefs = await SharedPreferences.getInstance(); - String? tk = await prefs.getString("token"); + String? tk = prefs.getString("token"); return tk; } diff --git a/lib/fluent/login.dart b/lib/fluent/login.dart index 3984966..3ece356 100644 --- a/lib/fluent/login.dart +++ b/lib/fluent/login.dart @@ -6,6 +6,8 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:momo/models/login_resp.dart'; +import 'package:momo/provider/token.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LoginPage extends StatelessWidget { @@ -59,21 +61,25 @@ class _LoginFormState extends State { if (usernameController.text.isNotEmpty && passwordController.text.isNotEmpty) { http - .post(Uri.parse("http://localhost:8080/user/login"), + .post(Uri.parse("http://192.168.110.156:8080/user/login"), headers: {"Content-Type": "application/json"}, body: jsonEncode({ "username": usernameController.text, "password": passwordController.text })) .then((resp) { + print(resp.body); if (resp.statusCode == HttpStatus.ok) { LoginResp loginResp = LoginResp.fromJson(jsonDecode(resp.body)); - SharedPreferences.getInstance().then((prefs) { - return prefs.setString("token", loginResp.token); - }).then((value) { - context.go("/"); - }); + print(loginResp.token); + // Provider.of(context).setToken(loginResp.token); + context.go("/"); + // SharedPreferences.getInstance().then((prefs) { + // prefs.setString("token", loginResp.token); + // }); + // .then((value) { + // }); } }); } diff --git a/lib/main.dart b/lib/main.dart index 2088438..6d6d563 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,13 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:momo/material/app.dart'; +import 'package:momo/provider/token.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:tray_manager/tray_manager.dart'; +// import 'package:tray_manager/tray_manager.dart'; void main() async { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { @@ -24,14 +28,38 @@ void main() async { await windowManager.focus(); }); } - runApp(const ProviderScope(child: MyApp())); + final prefs = await SharedPreferences.getInstance(); + String? tk = prefs.getString("token"); + final container = ProviderContainer(); + container.read(tokenProvider.notifier).setToken(tk); + runApp( + UncontrolledProviderScope( + container: container, + child: const MyApp(), + ), + ); + // runApp((child: MyApp(token: tk))); + // runApp(MultiProvider( + // providers: [ + // ChangeNotifierProvider( + // create: (context) => Secret(), + // ), + // ], + // child: MyApp( + // token: tk, + // ), + // )); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + const MyApp({super.key, this.token}); + + final String? token; @override Widget build(BuildContext context) { + // Provider.of(context, listen: false).setToken(token); + return const MyMaterialApp(); } } diff --git a/lib/material/app.dart b/lib/material/app.dart index 433510d..463eadb 100644 --- a/lib/material/app.dart +++ b/lib/material/app.dart @@ -1,49 +1,62 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:momo/material/router.dart'; import 'package:momo/provider/token.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; class MyMaterialApp extends ConsumerWidget { const MyMaterialApp({Key? key}) : super(key: key); Future loadToken() async { final prefs = await SharedPreferences.getInstance(); - String? tk = await prefs.getString("token"); + String? tk = prefs.getString("token"); return tk; } @override Widget build(BuildContext context, WidgetRef ref) { - return FutureBuilder( - future: loadToken(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - String? token = snapshot.data; - if (token != null) { - // ref - // .watch( - // tokenProvider.notifier, - // ) - // .setToken(token); - } - MyMaterialRouterConfig myMaterialRouterConfig = - MyMaterialRouterConfig(token); + String? token = ref.watch(tokenProvider); + MyMaterialRouterConfig myMaterialRouterConfig = + MyMaterialRouterConfig(token); - return MaterialApp.router( - routerConfig: myMaterialRouterConfig.router, - theme: ThemeData( - useMaterial3: true, scaffoldBackgroundColor: Colors.white), - ); - } else { - return MaterialApp( - theme: ThemeData( - useMaterial3: true, scaffoldBackgroundColor: Colors.white), - home: const Center( - child: CircularProgressIndicator(), - ), - ); - } - }); + return MaterialApp.router( + routerConfig: myMaterialRouterConfig.router, + debugShowCheckedModeBanner: false, + theme: + ThemeData(useMaterial3: true, scaffoldBackgroundColor: Colors.white), + ); + // return FutureBuilder( + // future: loadToken(), + // builder: (BuildContext context, AsyncSnapshot snapshot) { + // if (snapshot.connectionState == ConnectionState.done) { + // String? token = snapshot.data; + // if (token != null) { + // // ref + // // .watch( + // // tokenProvider.notifier, + // // ) + // // .setToken(token); + // } + // MyMaterialRouterConfig myMaterialRouterConfig = + // MyMaterialRouterConfig(token); + // + // return MaterialApp.router( + // routerConfig: myMaterialRouterConfig.router, + // theme: ThemeData( + // useMaterial3: true, scaffoldBackgroundColor: Colors.white), + // ); + // } else { + // return MaterialApp( + // theme: ThemeData( + // useMaterial3: true, scaffoldBackgroundColor: Colors.white), + // home: const Center( + // child: CircularProgressIndicator(), + // ), + // ); + // } + // }); } } diff --git a/lib/material/gallery.dart b/lib/material/gallery.dart index 991d531..81e3cd6 100644 --- a/lib/material/gallery.dart +++ b/lib/material/gallery.dart @@ -1,8 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:momo/models/image_list_resp.dart'; +import 'package:momo/models/image_resp.dart'; import 'package:momo/provider/token.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:go_router/go_router.dart'; +// import 'package:momo/provider/token.dart'; +// import 'package:provider/provider.dart'; + class Gallery extends ConsumerStatefulWidget { const Gallery({Key? key}) : super(key: key); @@ -11,17 +21,47 @@ class Gallery extends ConsumerStatefulWidget { } class _GalleryState extends ConsumerState { + Future loadImages(tk) async { + http.Response resp = await http.get( + Uri.parse("http://192.168.110.156:8080/image/history"), + headers: {"Authorization": tk}); + return resp.body; + } + + List imageList = []; + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + @override Widget build(BuildContext context) { - String token = ref.watch(tokenProvider); - print(token); - return Center( - child: ElevatedButton( - onPressed: () { - // ref.watch(tokenProvider.notifier).removeToken(); - context.go("/login"); - }, - child: Text("exit")), - ); + String? tk = ref.watch(tokenProvider); + if (tk != null) { + loadImages(tk).then((bd) { + if (mounted) { + setState(() { + imageList = ImageListResp.fromJson(jsonDecode(bd)).list; + }); + } + }); + } + return GridView.builder( + itemCount: imageList.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: MediaQuery.of(context).size.width > 640 ? 5 : 3), + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () { + context.go("/detail"); + }, + child: Image.network( + "http://192.168.110.156:8080/image/thumbnail/${imageList[index].file_path}", + fit: BoxFit.cover, + ), + ); + }); } } diff --git a/lib/material/home.dart b/lib/material/home.dart index 209ddb9..be9d13d 100644 --- a/lib/material/home.dart +++ b/lib/material/home.dart @@ -1,19 +1,32 @@ +import 'package:dio/dio.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:momo/material/gallery.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:mime/mime.dart'; +import 'package:momo/provider/token.dart'; -class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); +class HomePage extends ConsumerStatefulWidget { + const HomePage({Key? key, required this.content}) : super(key: key); + final Widget content; @override - State createState() => _HomePageState(); + ConsumerState createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends ConsumerState { int selectedIndex = 0; + final tabList = [ + { + "path": "/", + }, + {"path": "/profile"} + ]; @override Widget build(BuildContext context) { + String? token = ref.watch(tokenProvider); return Scaffold( appBar: AppBar( title: const Text( @@ -22,6 +35,39 @@ class _HomePageState extends State { ), elevation: 10, ), + floatingActionButton: selectedIndex == 0 + ? FloatingActionButton( + onPressed: () async { + if (token == null) { + return; + } + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: FileType.image); + + if (result != null) { + String? filePath = result.files.first.path; + if (filePath == null) { + return; + } + Dio dio = Dio(); + String? mimeType = lookupMimeType(filePath); + FormData data = FormData.fromMap({ + "pic": await MultipartFile.fromFile(filePath, + filename: result.files.first.name, + contentType: MediaType(mimeType!.split("/").first, + mimeType.split("/").last)) + }); + Response resp = await dio.post( + "http://192.168.110.156:8080/image/upload", + data: data, + options: Options(headers: {"Authorization": token})); + } else {} + + // context.go("/login"); + }, + child: const Icon(Icons.add), + ) + : null, body: Row( children: [ MediaQuery.of(context).size.width > 640 @@ -39,6 +85,7 @@ class _HomePageState extends State { setState(() { selectedIndex = idx; }); + context.go(tabList[idx]["path"] ?? ""); }, destinations: const [ NavigationRailDestination( @@ -51,7 +98,7 @@ class _HomePageState extends State { : const SizedBox( width: 0, ), - Expanded(child: pageList[selectedIndex]) + Expanded(child: widget.content) ], ), bottomNavigationBar: MediaQuery.of(context).size.width <= 640 @@ -72,10 +119,3 @@ class _HomePageState extends State { ); } } - -List pageList = [ - Gallery(), - Scaffold( - body: Placeholder(), - ) -]; diff --git a/lib/material/login.dart b/lib/material/login.dart index 3bad2f9..d4ef28d 100644 --- a/lib/material/login.dart +++ b/lib/material/login.dart @@ -3,10 +3,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:momo/models/login_resp.dart'; import 'package:momo/provider/token.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LoginPage extends StatelessWidget { @@ -75,24 +78,24 @@ class _LoginFormState extends ConsumerState { onPressed: () async { if (usernameController.text.isNotEmpty && passwordController.text.isNotEmpty) { - http.Response resp = await http.post( - Uri.parse("http://localhost:8080/user/login"), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "username": usernameController.text, - "password": passwordController.text - })); - - if (resp.statusCode == HttpStatus.ok) { - LoginResp loginResp = - LoginResp.fromJson(jsonDecode(resp.body)); - SharedPreferences prefs = - await SharedPreferences.getInstance(); - // ref.watch(tokenProvider.notifier).setToken(loginResp.token); - prefs.setString("token", loginResp.token).then((value) { - context.go("/"); - }); - } + http + .post(Uri.parse("http://192.168.110.156:8080/user/login"), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "username": usernameController.text, + "password": passwordController.text + })) + .then((resp) { + if (resp.statusCode == HttpStatus.ok) { + LoginResp loginResp = + LoginResp.fromJson(jsonDecode(resp.body)); + ref.watch(tokenProvider.notifier).setToken(loginResp.token); + // context.go("/"); + // }); + } + }).catchError((err) { + print(err.toString()); + }); } }), ], diff --git a/lib/material/profile.dart b/lib/material/profile.dart new file mode 100644 index 0000000..29ee929 --- /dev/null +++ b/lib/material/profile.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:momo/provider/token.dart'; + +class Profile extends ConsumerWidget { + const Profile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Center( + child: ElevatedButton( + onPressed: () { + ref.watch(tokenProvider.notifier).removeToken(); + }, + child: const Text('quit')), + ); + } +} diff --git a/lib/material/router.dart b/lib/material/router.dart index 32ef214..630979f 100644 --- a/lib/material/router.dart +++ b/lib/material/router.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:momo/material/gallery.dart'; import 'package:momo/material/home.dart'; import 'package:momo/material/login.dart'; +import 'package:momo/material/profile.dart'; class MyMaterialRouterConfig { late GoRouter router; @@ -9,23 +11,65 @@ class MyMaterialRouterConfig { MyMaterialRouterConfig(String? token) { router = GoRouter( routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return HomePage( + content: child, + ); + }, + routes: [ + GoRoute( + path: "/", + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: Gallery()), + redirect: (BuildContext context, GoRouterState state) { + if (token == null || token.isEmpty) { + return '/login'; + } + return null; + }), + GoRoute( + path: "/profile", + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: Profile())), + GoRoute( + path: "/detail", + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage( + child: Center( + child: Text("detail"), + ))) + ], + ), GoRoute( - path: '/', + path: "/login", builder: (BuildContext context, GoRouterState state) { - return const HomePage(); + return const LoginPage(); }, redirect: (BuildContext context, GoRouterState state) { - if (token == null || token.isEmpty) { - return '/login'; + if (token != null && token.isNotEmpty) { + return '/'; } return null; }), - GoRoute( - path: "/login", - builder: (BuildContext context, GoRouterState state) { - return const LoginPage(); - }, - ) + // ShellRoute( + // builder: (BuildContext context, GoRouterState state, Widget child) { + // return Scaffold( + // appBar: AppBar( + // title: Text("nest"), + // ), + // body: child, + // ); + // }, + // routes: [ + // GoRoute( + // path: '/', + // builder: (BuildContext context, GoRouterState state) { + // return Center( + // child: Text("login"), + // ); + // }) + // ]), ], ); } diff --git a/lib/models/image_list_resp.dart b/lib/models/image_list_resp.dart new file mode 100644 index 0000000..946564e --- /dev/null +++ b/lib/models/image_list_resp.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:momo/models/image_resp.dart'; + +part 'image_list_resp.g.dart'; + +@JsonSerializable() +class ImageListResp { + List list; + int total; + + ImageListResp(this.list, this.total); + + factory ImageListResp.fromJson(Map json) => + _$ImageListRespFromJson(json); + + Map toJson() => _$ImageListRespToJson(this); +} diff --git a/lib/models/image_list_resp.g.dart b/lib/models/image_list_resp.g.dart new file mode 100644 index 0000000..ac531e5 --- /dev/null +++ b/lib/models/image_list_resp.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_list_resp.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageListResp _$ImageListRespFromJson(Map json) => + ImageListResp( + (json['list'] as List) + .map((e) => ImageResp.fromJson(e as Map)) + .toList(), + json['total'] as int, + ); + +Map _$ImageListRespToJson(ImageListResp instance) => + { + 'list': instance.list, + 'total': instance.total, + }; diff --git a/lib/models/image_resp.dart b/lib/models/image_resp.dart new file mode 100644 index 0000000..92d456e --- /dev/null +++ b/lib/models/image_resp.dart @@ -0,0 +1,30 @@ +import 'dart:ffi'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'image_resp.g.dart'; + +@JsonSerializable() +class ImageResp { + int id; + String file_name; + String file_path; + int upload_time; + int size; + int width; + int height; + + ImageResp(this.id, this.file_name, this.file_path, this.upload_time, + this.size, this.width, this.height); + + /// A necessary factory constructor for creating a new Configs instance + /// from a map. Pass the map to the generated `_$ConfigsFromJson()` constructor. + /// The constructor is named after the source class, in this case, Configs. + factory ImageResp.fromJson(Map json) => + _$ImageRespFromJson(json); + + /// `toJson` is the convention for a class to declare support for serialization + /// to JSON. The implementation simply calls the private, generated + /// helper method `_$ImageRespToJson`. + Map toJson() => _$ImageRespToJson(this); +} diff --git a/lib/models/image_resp.g.dart b/lib/models/image_resp.g.dart new file mode 100644 index 0000000..774e141 --- /dev/null +++ b/lib/models/image_resp.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_resp.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageResp _$ImageRespFromJson(Map json) => ImageResp( + json['id'] as int, + json['file_name'] as String, + json['file_path'] as String, + json['upload_time'] as int, + json['size'] as int, + json['width'] as int, + json['height'] as int, + ); + +Map _$ImageRespToJson(ImageResp instance) => { + 'id': instance.id, + 'file_name': instance.file_name, + 'file_path': instance.file_path, + 'upload_time': instance.upload_time, + 'size': instance.size, + 'width': instance.width, + 'height': instance.height, + }; diff --git a/lib/provider/token.dart b/lib/provider/token.dart index 942b27e..51b63c0 100644 --- a/lib/provider/token.dart +++ b/lib/provider/token.dart @@ -1,16 +1,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class TokenNotifier extends Notifier { +class TokenNotifier extends Notifier { @override - String build() { + String? build() { return ""; } - setToken(String token) { + setToken(String? token) { state = token; SharedPreferences.getInstance().then((prefs) { - prefs.setString("token", token); + prefs.setString("token", token ?? ""); }); } @@ -23,4 +23,28 @@ class TokenNotifier extends Notifier { } final tokenProvider = - NotifierProvider(() => TokenNotifier()); + NotifierProvider(() => TokenNotifier()); + +// import 'package:fluent_ui/fluent_ui.dart'; +// import 'package:fluent_ui/fluent_ui.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// +// class Secret extends ChangeNotifier { +// String? token; +// +// void setToken(String? data) { +// token = data; +// SharedPreferences.getInstance().then((prefs) { +// prefs.setString("token", data ?? ""); +// }); +// // notifyListeners(); +// // notifyListeners(); +// } +// +// void removeToken() { +// token = ""; +// SharedPreferences.getInstance().then((prefs) { +// prefs.setString("token", ""); +// }); +// } +// } diff --git a/pubspec.lock b/pubspec.lock index aafbe89..b344bb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.4" + dio: + dependency: "direct main" + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.0.6" fake_async: dependency: transitive description: @@ -225,6 +233,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.2.5" fixnum: dependency: transitive description: @@ -275,6 +291,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.7" flutter_riverpod: dependency: "direct main" description: @@ -350,7 +374,7 @@ packages: source: hosted version: "3.2.1" http_parser: - dependency: transitive + dependency: "direct main" description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" @@ -454,7 +478,7 @@ packages: source: hosted version: "1.8.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e @@ -469,6 +493,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.7.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -557,6 +589,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.0.5" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2eb196f..e5711fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,11 @@ dependencies: flutter_riverpod: ^2.1.3 window_manager: ^0.3.0 tray_manager: ^0.2.0 + provider: ^6.0.5 + file_picker: ^5.2.5 + dio: ^4.0.6 + http_parser: ^4.0.2 + mime: ^1.0.4 dev_dependencies: flutter_test: