feat: send request handling

This commit is contained in:
Tien Do Nam
2022-12-21 03:39:43 +01:00
parent e2ac195aca
commit e9eeeaa027
25 changed files with 506 additions and 102 deletions

View File

@@ -1,6 +1,8 @@
{
"appName": "LocalSend",
"general": {
"accept": "Accept",
"decline": "Decline",
"unknown": "Unknown"
},
"receive": {
@@ -46,5 +48,11 @@
"alias": "Alias",
"port": "Port"
}
},
"receivePage": {
"subTitle": {
"one": "wants to send you a file.",
"other": "wants to send you {n} files."
}
}
}

View File

@@ -1,6 +1,8 @@
{
"appName": "LocalSend",
"general": {
"accept": "Akzeptieren",
"decline": "Ablehnen",
"unknown": "Unbekannt"
},
"receive": {
@@ -46,5 +48,11 @@
"alias": "Alias",
"port": "Port"
}
},
"receivePage": {
"subTitle": {
"one": "möchte dir eine Datei senden.",
"other": "möchte dir {n} Dateien senden."
}
}
}

View File

@@ -1,11 +1,14 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/pages/home_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/routes.dart';
import 'package:localsend_app/service/persistence_service.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/device_info_helper.dart';
import 'package:window_manager/window_manager.dart';
Future<void> main() async {
@@ -25,9 +28,12 @@ Future<void> main() async {
LocaleSettings.setLocale(locale);
}
final deviceInfo = await getDeviceInfo();
runApp(TranslationProvider(
child: ProviderScope(
overrides: [
deviceInfoProvider.overrideWithValue(deviceInfo),
settingsProvider.overrideWith((ref) => SettingsNotifier(persistenceService)),
],
child: const LocalSendApp(),
@@ -36,18 +42,21 @@ Future<void> main() async {
}
class LocalSendApp extends ConsumerWidget {
static final router = GoRouter(routes: $appRoutes);
static BuildContext get routerContext => router.routerDelegate.navigatorKey.currentContext!;
const LocalSendApp();
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(settingsProvider.select((settings) => settings.theme));
return MaterialApp(
return MaterialApp.router(
title: t.appName,
debugShowCheckedModeBanner: false,
theme: getTheme(Brightness.light),
darkTheme: getTheme(Brightness.dark),
themeMode: themeMode,
home: const HomePage(),
routerConfig: router,
);
}
}

View File

@@ -18,6 +18,7 @@ enum DeviceType {
class Device with _$Device {
const factory Device({
required String ip,
required int port,
required String alias,
required String? deviceModel,
required DeviceType deviceType,

View File

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/device.dart';
part 'file_dto.freezed.dart';
part 'file_dto.g.dart';
/// Categorization of one file.
/// We use this information for a better UX.
enum FileType {
image,
video,
pdf,
text,
other,
}
@freezed
class FileDto with _$FileDto {
const factory FileDto({
required int id, // unique inside from send batch
required String fileName,
required int size,
required DeviceType deviceType,
}) = _FileDto;
factory FileDto.fromJson(Map<String, Object?> json) => _$FileDtoFromJson(json);
}

View File

@@ -14,3 +14,15 @@ class InfoDto with _$InfoDto {
factory InfoDto.fromJson(Map<String, Object?> json) => _$InfoDtoFromJson(json);
}
extension InfoToDeviceExt on InfoDto {
Device toDevice(String ip, int port) {
return Device(
ip: ip,
port: port,
alias: alias,
deviceModel: deviceModel,
deviceType: deviceType,
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
part 'send_request_dto.freezed.dart';
part 'send_request_dto.g.dart';
@freezed
class SendRequestDto with _$SendRequestDto {
const factory SendRequestDto({
required InfoDto info,
required List<FileDto> files,
}) = _SendRequestDto;
factory SendRequestDto.fromJson(Map<String, Object?> json) => _$SendRequestDtoFromJson(json);
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
part 'expected_file.freezed.dart';
@freezed
class ExpectedFile with _$ExpectedFile {
const factory ExpectedFile({
required String token,
required FileDto file,
required String? tempPath, // file is saved to a temporary path first
}) = _ExpectedFile;
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/server/expected_file.dart';
part 'receive_state.freezed.dart';
@freezed
class ReceiveState with _$ReceiveState {
const factory ReceiveState({
required Device sender,
required Map<int, ExpectedFile> files,
}) = _ReceiveState;
}

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/server/receive_state.dart';
import 'package:localsend_app/model/server/temp_request.dart';
part 'server_state.freezed.dart';
@@ -10,5 +12,7 @@ class ServerState with _$ServerState {
required HttpServer httpServer,
required String alias,
required int port,
required TempRequest? tempRequest, // will become a [ReceiveState] on accept
required ReceiveState? receiveState,
}) = _ServerState;
}

View File

@@ -0,0 +1,16 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
part 'temp_request.freezed.dart';
@freezed
class TempRequest with _$TempRequest {
const factory TempRequest({
required Device sender,
required List<FileDto> files,
required StreamController<bool> responseHandler, // use this to accept / decline the request
}) = _TempRequest;
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/pages/tabs/receive_page.dart';
import 'package:localsend_app/pages/tabs/send_page.dart';
import 'package:localsend_app/pages/tabs/settings_page.dart';
import 'package:localsend_app/pages/tabs/receive_tab.dart';
import 'package:localsend_app/pages/tabs/send_tab.dart';
import 'package:localsend_app/pages/tabs/settings_tab.dart';
enum _Tab {
receive(Icons.wifi),
@@ -43,9 +43,9 @@ class _HomePageState extends State<HomePage> {
child: IndexedStack(
index: _currentTab.index,
children: const [
ReceivePage(),
SendPage(),
SettingsPage(),
ReceiveTab(),
SendTab(),
SettingsTab(),
],
),
),

101
lib/pages/receive_page.dart Normal file
View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/server_provider.dart';
import 'package:localsend_app/util/ip_helper.dart';
import 'package:localsend_app/widget/device_bage.dart';
class ReceivePage extends ConsumerWidget {
const ReceivePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final tempRequest = ref.watch(serverProvider)?.tempRequest;
if (tempRequest == null) {
// when declining/accepting the request, there is a short frame where tempRequest is null
return Scaffold(
body: Container(),
);
}
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
child: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(tempRequest.sender.deviceType.icon, size: 64),
const SizedBox(height: 10),
Text(
tempRequest.sender.alias,
style: const TextStyle(fontSize: 48),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DeviceBadge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: '#${tempRequest.sender.ip.visualId}',
),
if (tempRequest.sender.deviceModel != null)
Padding(
padding: const EdgeInsets.only(left: 10),
child: DeviceBadge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: tempRequest.sender.deviceModel!,
),
),
],
),
const SizedBox(height: 40),
Text(
t.receivePage.subTitle(n: tempRequest.files.length),
style: Theme.of(context).textTheme.headline6,
textAlign: TextAlign.center,
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).buttonTheme.colorScheme!.error,
foregroundColor: Theme.of(context).buttonTheme.colorScheme!.onError
),
onPressed: () {
ref.read(serverProvider.notifier).declineFileRequest();
context.pop();
},
icon: const Icon(Icons.close),
label: Text(t.general.decline),
),
const SizedBox(width: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).buttonTheme.colorScheme!.primary,
foregroundColor: Theme.of(context).buttonTheme.colorScheme!.onPrimary,
),
onPressed: () {
ref.read(serverProvider.notifier).acceptFileRequest();
context.pop();
},
icon: const Icon(Icons.check_circle),
label: Text(t.general.accept),
),
],
),
],
),
),
),
);
}
}

View File

@@ -8,14 +8,14 @@ import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/ip_helper.dart';
import 'package:localsend_app/widget/rotating_widget.dart';
class ReceivePage extends ConsumerStatefulWidget {
const ReceivePage({Key? key}) : super(key: key);
class ReceiveTab extends ConsumerStatefulWidget {
const ReceiveTab({Key? key}) : super(key: key);
@override
ConsumerState<ReceivePage> createState() => _ReceivePageState();
ConsumerState<ReceiveTab> createState() => _ReceiveTagState();
}
class _ReceivePageState extends ConsumerState<ReceivePage> {
class _ReceiveTagState extends ConsumerState<ReceiveTab> {
bool _advanced = false;
@override

View File

@@ -1,21 +1,26 @@
import 'package:collection/collection.dart';
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:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
import 'package:localsend_app/model/dto/send_request_dto.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/nearby_devices_provider.dart';
import 'package:localsend_app/provider/selected_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/file_size_helper.dart';
import 'package:localsend_app/widget/device_widget.dart';
import 'package:localsend_app/widget/device_list_tile.dart';
class SendPage extends ConsumerStatefulWidget {
const SendPage({Key? key}) : super(key: key);
class SendTab extends ConsumerStatefulWidget {
const SendTab({Key? key}) : super(key: key);
@override
ConsumerState<SendPage> createState() => _SendPageState();
ConsumerState<SendTab> createState() => _SendTabState();
}
class _SendPageState extends ConsumerState<SendPage> {
class _SendTabState extends ConsumerState<SendTab> {
@override
void initState() {
super.initState();
@@ -23,6 +28,8 @@ class _SendPageState extends ConsumerState<SendPage> {
@override
Widget build(BuildContext context) {
final deviceInfo = ref.watch(deviceInfoProvider);
final settings = ref.watch(settingsProvider);
final selectedFiles = ref.watch(selectedFilesProvider);
final devices = ref.watch(nearbyDevicesProvider);
return ListView(
@@ -88,7 +95,30 @@ class _SendPageState extends ConsumerState<SendPage> {
return data.map((device) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: DeviceWidget(device: device),
child: DeviceListTile(
device: device,
onTap: () async {
final url = 'http://${device.ip}:${device.port}/localsend/v1/send-request';
final dio = Dio(BaseOptions(
connectTimeout: 30 * 1000,
sendTimeout: 30 * 1000,
));
try {
final response = await dio.post(url,
data: SendRequestDto(
info: InfoDto(
alias: settings.alias,
deviceModel: deviceInfo.deviceModel,
deviceType: deviceInfo.deviceType,
),
files: [],
).toJson());
print('Response: ${response.statusCode}, ${response.data.runtimeType}');
} on DioError catch (e) {
print(e);
}
},
),
);
});
},

View File

@@ -4,14 +4,14 @@ import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/server_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({Key? key}) : super(key: key);
class SettingsTab extends ConsumerStatefulWidget {
const SettingsTab({Key? key}) : super(key: key);
@override
ConsumerState<SettingsPage> createState() => _SettingsPageState();
ConsumerState<SettingsTab> createState() => _SettingsTabState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
class _SettingsTabState extends ConsumerState<SettingsTab> {
final _aliasController = TextEditingController();
final _portController = TextEditingController();

View File

@@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/util/device_info_helper.dart';
final deviceInfoProvider = Provider<DeviceInfoResult>((ref) {
throw Exception('settingsProvider not initialized');
});

View File

@@ -1,19 +1,37 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/main.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
import 'package:localsend_app/model/server_state.dart';
import 'package:localsend_app/model/dto/send_request_dto.dart';
import 'package:localsend_app/model/server/expected_file.dart';
import 'package:localsend_app/model/server/receive_state.dart';
import 'package:localsend_app/model/server/server_state.dart';
import 'package:localsend_app/model/server/temp_request.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/routes.dart';
import 'package:localsend_app/service/persistence_service.dart';
import 'package:localsend_app/util/alias_generator.dart';
import 'package:localsend_app/util/device_info_helper.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:uuid/uuid.dart';
final serverProvider = StateNotifierProvider<ServerNotifier, ServerState?>((ref) => ServerNotifier());
final serverProvider = StateNotifierProvider<ServerNotifier, ServerState?>((ref) {
final deviceInfo = ref.watch(deviceInfoProvider);
return ServerNotifier(deviceInfo);
});
const _basePath = '/localsend/v1';
const _uuid = Uuid();
class ServerNotifier extends StateNotifier<ServerState?> {
ServerNotifier() : super(null);
final DeviceInfoResult deviceInfo;
ServerNotifier(this.deviceInfo) : super(null);
Future<ServerState?> startServer({required String alias, required int port}) async {
if (state != null) {
@@ -32,8 +50,7 @@ class ServerNotifier extends StateNotifier<ServerState?> {
final app = Router();
final deviceInfo = await getDeviceInfo();
app.get('/localsend/v1/info', (Request request) {
app.get('$_basePath/info', (Request request) {
final dto = InfoDto(
alias: alias,
deviceModel: deviceInfo.deviceModel,
@@ -42,8 +59,34 @@ class ServerNotifier extends StateNotifier<ServerState?> {
return Response.ok(jsonEncode(dto.toJson()), headers: {'Content-Type': 'application/json'});
});
app.get('/user/<user>', (Request request, String user) {
return Response.ok('hello $user');
app.post('$_basePath/send-request', (Request request) async {
if (state!.tempRequest != null || state!.receiveState != null) {
// block incoming requests when we are already in a session
return Response.badRequest();
}
final ip = request.context['shelf.io.connection_info'] as HttpConnectionInfo;
final payload = await request.readAsString();
final dto = SendRequestDto.fromJson(jsonDecode(payload));
final streamController = StreamController<bool>();
state = state!.copyWith(
tempRequest: TempRequest(
sender: dto.info.toDevice(ip.remoteAddress.address, port),
files: dto.files,
responseHandler: streamController,
),
);
// ignore: use_build_context_synchronously
const ReceiveRoute().push(LocalSendApp.routerContext);
// Delayed response (waiting for user's decision)
final result = await streamController.stream.first;
if (result) {
return Response.ok('');
} else {
return Response.badRequest();
}
});
print('Starting server...');
@@ -53,6 +96,8 @@ class ServerNotifier extends StateNotifier<ServerState?> {
httpServer: await serve(app, '0.0.0.0', port),
alias: alias,
port: port,
tempRequest: null,
receiveState: null,
);
print('Server started. (Port: ${newServerState.port})');
} catch (e) {
@@ -73,4 +118,41 @@ class ServerNotifier extends StateNotifier<ServerState?> {
await stopServer();
return await startServer(alias: alias, port: port);
}
void acceptFileRequest() {
final tempRequest = state?.tempRequest;
if (tempRequest == null) {
return;
}
tempRequest.responseHandler.add(true);
tempRequest.responseHandler.close();
state = state?.copyWith(
tempRequest: null,
receiveState: ReceiveState(
sender: tempRequest.sender,
files: {
for (final file in tempRequest.files)
file.id: ExpectedFile(
token: _uuid.v4(),
file: file,
tempPath: null,
),
},
),
);
}
void declineFileRequest() {
final controller = state?.tempRequest?.responseHandler;
if (controller == null) {
return;
}
controller.add(false);
controller.close();
state = state?.copyWith(
tempRequest: null,
receiveState: null,
);
}
}

22
lib/routes.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:localsend_app/pages/home_page.dart';
import 'package:localsend_app/pages/receive_page.dart';
part 'routes.g.dart';
@TypedGoRoute<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
const HomeRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const HomePage();
}
@TypedGoRoute<ReceiveRoute>(path: '/receive')
class ReceiveRoute extends GoRouteData {
const ReceiveRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const ReceivePage();
}

View File

@@ -67,12 +67,7 @@ class PollingService {
try {
final response = await _dio.get(url);
final dto = InfoDto.fromJson(response.data);
device = Device(
ip: currentIp,
alias: dto.alias,
deviceModel: dto.deviceModel,
deviceType: dto.deviceType,
);
device = dto.toDevice(currentIp, port);
} on DioError catch (e) {
device = null;
// print('$url: ${e.error}');

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class DeviceBadge extends StatelessWidget {
final Color color;
final String label;
const DeviceBadge({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(5),
),
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onInverseSurface)),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/util/ip_helper.dart';
import 'package:localsend_app/widget/device_bage.dart';
class DeviceListTile extends StatelessWidget {
final Device device;
final VoidCallback? onTap;
const DeviceListTile({required this.device, this.onTap});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
children: [
Icon(device.deviceType.icon, size: 46),
const SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(device.alias, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 5),
Wrap(
runSpacing: 10,
spacing: 10,
children: [
DeviceBadge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: '#${device.ip.visualId}',
),
if (device.deviceModel != null)
DeviceBadge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: device.deviceModel!,
),
],
),
],
),
],
),
),
),
);
}
}

View File

@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/util/ip_helper.dart';
class DeviceWidget extends StatelessWidget {
final Device device;
const DeviceWidget({required this.device});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
children: [
Icon(device.deviceType.icon, size: 46),
const SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(device.alias, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 5),
Wrap(
runSpacing: 10,
spacing: 10,
children: [
_Badge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: '#${device.ip.visualId}',
),
if (device.deviceModel != null)
_Badge(
color: Theme.of(context).colorScheme.tertiaryContainer,
label: device.deviceModel!,
),
],
),
],
),
],
),
),
);
}
}
class _Badge extends StatelessWidget {
final Color color;
final String label;
const _Badge({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(5),
),
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onInverseSurface)),
);
}
}

View File

@@ -291,6 +291,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
go_router:
dependency: "direct main"
description:
name: go_router
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
go_router_builder:
dependency: "direct dev"
description:
name: go_router_builder
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.15"
graphs:
dependency: transitive
description:
@@ -452,6 +466,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
path_to_regexp:
dependency: transitive
description:
name: path_to_regexp
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
petitparser:
dependency: transitive
description:
@@ -702,6 +723,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
@@ -760,4 +788,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.18.5 <3.0.0"
flutter: ">=3.0.0"
flutter: ">=3.3.0"

View File

@@ -18,12 +18,14 @@ dependencies:
file_picker: 5.2.4
flutter_riverpod: 2.1.1
freezed_annotation: 2.2.0
go_router: 6.0.0
json_annotation: 4.7.0
shared_preferences: 2.0.15
shelf: 1.4.0
shelf_router: 1.1.3
slang: 3.7.0
slang_flutter: 3.7.0
uuid: 3.0.7
window_manager: 0.2.8
dev_dependencies:
@@ -31,6 +33,7 @@ dev_dependencies:
flutter_gen_runner: 5.1.0+1
flutter_lints: 2.0.1
freezed: 2.3.2
go_router_builder: 1.0.15
json_serializable: 6.5.4
slang_build_runner: 3.7.0