mirror of
https://github.com/localsend/localsend.git
synced 2026-04-29 03:00:23 -04:00
feat: send request handling
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
27
lib/model/dto/file_dto.dart
Normal file
27
lib/model/dto/file_dto.dart
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
16
lib/model/dto/send_request_dto.dart
Normal file
16
lib/model/dto/send_request_dto.dart
Normal 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);
|
||||
}
|
||||
13
lib/model/server/expected_file.dart
Normal file
13
lib/model/server/expected_file.dart
Normal 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;
|
||||
}
|
||||
13
lib/model/server/receive_state.dart
Normal file
13
lib/model/server/receive_state.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
16
lib/model/server/temp_request.dart
Normal file
16
lib/model/server/temp_request.dart
Normal 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;
|
||||
}
|
||||
@@ -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
101
lib/pages/receive_page.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -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();
|
||||
|
||||
6
lib/provider/device_info_provider.dart
Normal file
6
lib/provider/device_info_provider.dart
Normal 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');
|
||||
});
|
||||
@@ -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
22
lib/routes.dart
Normal 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();
|
||||
}
|
||||
@@ -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}');
|
||||
|
||||
20
lib/widget/device_bage.dart
Normal file
20
lib/widget/device_bage.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/widget/device_list_tile.dart
Normal file
55
lib/widget/device_list_tile.dart
Normal 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!,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
pubspec.lock
30
pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user