diff --git a/assets/i18n/strings.i18n.json b/assets/i18n/strings.i18n.json index 51e3569c..f5ac3dd9 100644 --- a/assets/i18n/strings.i18n.json +++ b/assets/i18n/strings.i18n.json @@ -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." + } } } diff --git a/assets/i18n/strings_de.i18n.json b/assets/i18n/strings_de.i18n.json index 59156a4e..03036503 100644 --- a/assets/i18n/strings_de.i18n.json +++ b/assets/i18n/strings_de.i18n.json @@ -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." + } } } diff --git a/lib/main.dart b/lib/main.dart index 1d3e93bd..a9496cf5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main() async { @@ -25,9 +28,12 @@ Future 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 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, ); } } diff --git a/lib/model/device.dart b/lib/model/device.dart index b1c98796..1d62f376 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -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, diff --git a/lib/model/dto/file_dto.dart b/lib/model/dto/file_dto.dart new file mode 100644 index 00000000..5387705f --- /dev/null +++ b/lib/model/dto/file_dto.dart @@ -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 json) => _$FileDtoFromJson(json); +} diff --git a/lib/model/dto/info_dto.dart b/lib/model/dto/info_dto.dart index c26d0f7e..e8d5adc3 100644 --- a/lib/model/dto/info_dto.dart +++ b/lib/model/dto/info_dto.dart @@ -14,3 +14,15 @@ class InfoDto with _$InfoDto { factory InfoDto.fromJson(Map 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, + ); + } +} diff --git a/lib/model/dto/send_request_dto.dart b/lib/model/dto/send_request_dto.dart new file mode 100644 index 00000000..08625593 --- /dev/null +++ b/lib/model/dto/send_request_dto.dart @@ -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 files, + }) = _SendRequestDto; + + factory SendRequestDto.fromJson(Map json) => _$SendRequestDtoFromJson(json); +} diff --git a/lib/model/server/expected_file.dart b/lib/model/server/expected_file.dart new file mode 100644 index 00000000..6ae01608 --- /dev/null +++ b/lib/model/server/expected_file.dart @@ -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; +} diff --git a/lib/model/server/receive_state.dart b/lib/model/server/receive_state.dart new file mode 100644 index 00000000..cbbbce9f --- /dev/null +++ b/lib/model/server/receive_state.dart @@ -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 files, + }) = _ReceiveState; +} diff --git a/lib/model/server_state.dart b/lib/model/server/server_state.dart similarity index 54% rename from lib/model/server_state.dart rename to lib/model/server/server_state.dart index 83edfc47..099331b3 100644 --- a/lib/model/server_state.dart +++ b/lib/model/server/server_state.dart @@ -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; } diff --git a/lib/model/server/temp_request.dart b/lib/model/server/temp_request.dart new file mode 100644 index 00000000..2f84c8f6 --- /dev/null +++ b/lib/model/server/temp_request.dart @@ -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 files, + required StreamController responseHandler, // use this to accept / decline the request + }) = _TempRequest; +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 0580105b..e979ca57 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -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 { child: IndexedStack( index: _currentTab.index, children: const [ - ReceivePage(), - SendPage(), - SettingsPage(), + ReceiveTab(), + SendTab(), + SettingsTab(), ], ), ), diff --git a/lib/pages/receive_page.dart b/lib/pages/receive_page.dart new file mode 100644 index 00000000..39d734f8 --- /dev/null +++ b/lib/pages/receive_page.dart @@ -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), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/tabs/receive_page.dart b/lib/pages/tabs/receive_tab.dart similarity index 96% rename from lib/pages/tabs/receive_page.dart rename to lib/pages/tabs/receive_tab.dart index 4ba93de9..a34ec768 100644 --- a/lib/pages/tabs/receive_page.dart +++ b/lib/pages/tabs/receive_tab.dart @@ -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 createState() => _ReceivePageState(); + ConsumerState createState() => _ReceiveTagState(); } -class _ReceivePageState extends ConsumerState { +class _ReceiveTagState extends ConsumerState { bool _advanced = false; @override diff --git a/lib/pages/tabs/send_page.dart b/lib/pages/tabs/send_tab.dart similarity index 67% rename from lib/pages/tabs/send_page.dart rename to lib/pages/tabs/send_tab.dart index ac292f62..afb127c5 100644 --- a/lib/pages/tabs/send_page.dart +++ b/lib/pages/tabs/send_tab.dart @@ -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 createState() => _SendPageState(); + ConsumerState createState() => _SendTabState(); } -class _SendPageState extends ConsumerState { +class _SendTabState extends ConsumerState { @override void initState() { super.initState(); @@ -23,6 +28,8 @@ class _SendPageState extends ConsumerState { @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 { 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); + } + }, + ), ); }); }, diff --git a/lib/pages/tabs/settings_page.dart b/lib/pages/tabs/settings_tab.dart similarity index 96% rename from lib/pages/tabs/settings_page.dart rename to lib/pages/tabs/settings_tab.dart index f37bee37..c9a17b2d 100644 --- a/lib/pages/tabs/settings_page.dart +++ b/lib/pages/tabs/settings_tab.dart @@ -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 createState() => _SettingsPageState(); + ConsumerState createState() => _SettingsTabState(); } -class _SettingsPageState extends ConsumerState { +class _SettingsTabState extends ConsumerState { final _aliasController = TextEditingController(); final _portController = TextEditingController(); diff --git a/lib/provider/device_info_provider.dart b/lib/provider/device_info_provider.dart new file mode 100644 index 00000000..20685907 --- /dev/null +++ b/lib/provider/device_info_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:localsend_app/util/device_info_helper.dart'; + +final deviceInfoProvider = Provider((ref) { + throw Exception('settingsProvider not initialized'); +}); diff --git a/lib/provider/server_provider.dart b/lib/provider/server_provider.dart index 8264317e..d6e974da 100644 --- a/lib/provider/server_provider.dart +++ b/lib/provider/server_provider.dart @@ -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((ref) => ServerNotifier()); +final serverProvider = StateNotifierProvider((ref) { + final deviceInfo = ref.watch(deviceInfoProvider); + return ServerNotifier(deviceInfo); +}); + +const _basePath = '/localsend/v1'; +const _uuid = Uuid(); class ServerNotifier extends StateNotifier { - ServerNotifier() : super(null); + final DeviceInfoResult deviceInfo; + + ServerNotifier(this.deviceInfo) : super(null); Future startServer({required String alias, required int port}) async { if (state != null) { @@ -32,8 +50,7 @@ class ServerNotifier extends StateNotifier { 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 { return Response.ok(jsonEncode(dto.toJson()), headers: {'Content-Type': 'application/json'}); }); - app.get('/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(); + 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 { 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 { 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, + ); + } } diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 00000000..5f4c000b --- /dev/null +++ b/lib/routes.dart @@ -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(path: '/') +class HomeRoute extends GoRouteData { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomePage(); +} + +@TypedGoRoute(path: '/receive') +class ReceiveRoute extends GoRouteData { + const ReceiveRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const ReceivePage(); +} diff --git a/lib/service/polling_service.dart b/lib/service/polling_service.dart index 314b98e9..83aaca39 100644 --- a/lib/service/polling_service.dart +++ b/lib/service/polling_service.dart @@ -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}'); diff --git a/lib/widget/device_bage.dart b/lib/widget/device_bage.dart new file mode 100644 index 00000000..c51f8922 --- /dev/null +++ b/lib/widget/device_bage.dart @@ -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)), + ); + } +} diff --git a/lib/widget/device_list_tile.dart b/lib/widget/device_list_tile.dart new file mode 100644 index 00000000..b1a59c52 --- /dev/null +++ b/lib/widget/device_list_tile.dart @@ -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!, + ), + ], + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widget/device_widget.dart b/lib/widget/device_widget.dart deleted file mode 100644 index f910a961..00000000 --- a/lib/widget/device_widget.dart +++ /dev/null @@ -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)), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 365807ba..4c6e30ca 100644 --- a/pubspec.lock +++ b/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" diff --git a/pubspec.yaml b/pubspec.yaml index a88d5eeb..8ee185ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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