From dba2fecb2d9f8604cb348a72b030d82c5fc0bc01 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 8 Feb 2023 21:32:57 +0100 Subject: [PATCH] feat: show error message in send page --- assets/i18n/strings.i18n.json | 6 +- lib/model/send/send_state.dart | 1 + lib/model/session_status.dart | 1 + lib/pages/send_page.dart | 34 +++++++- lib/provider/network/send_provider.dart | 105 +++++++++++++++--------- lib/widget/dialogs/error_dialog.dart | 23 ++++++ 6 files changed, 129 insertions(+), 41 deletions(-) create mode 100644 lib/widget/dialogs/error_dialog.dart diff --git a/assets/i18n/strings.i18n.json b/assets/i18n/strings.i18n.json index cd1f6392..79ee653d 100644 --- a/assets/i18n/strings.i18n.json +++ b/assets/i18n/strings.i18n.json @@ -126,7 +126,8 @@ }, "sendPage": { "waiting": "Waiting for response...", - "rejected": "The recipient has rejected the request." + "rejected": "The recipient has rejected the request.", + "busy": "The recipient is busy with another request." }, "progressPage": { "titleSending": "Sending files", @@ -246,6 +247,9 @@ "title": "Encryption disabled", "content": "Communication now takes place via the unencrypted HTTP protocol. To use HTTPS, enable encryption again." }, + "errorDialog": { + "title": "@:general.error" + }, "fileInfo": { "title": "File information", "fileName": "File name:", diff --git a/lib/model/send/send_state.dart b/lib/model/send/send_state.dart index de9da1e3..3d50684d 100644 --- a/lib/model/send/send_state.dart +++ b/lib/model/send/send_state.dart @@ -15,5 +15,6 @@ class SendState with _$SendState { required int? startTime, required int? endTime, required CancelToken? cancelToken, + required String? errorMessage, }) = _SendState; } diff --git a/lib/model/session_status.dart b/lib/model/session_status.dart index e09977f8..91aa5628 100644 --- a/lib/model/session_status.dart +++ b/lib/model/session_status.dart @@ -2,6 +2,7 @@ /// Both receiver and sender should share the same information. enum SessionStatus { waiting, // wait for receiver response (wait for decline / accept) + recipientBusy, // recipient is busy with another request (end of session) declined, // receiver declined the request (end of session) sending, // files are being sent finished, // all files sent (end of session) diff --git a/lib/pages/send_page.dart b/lib/pages/send_page.dart index 860a1fbf..ec3fdd0e 100644 --- a/lib/pages/send_page.dart +++ b/lib/pages/send_page.dart @@ -7,6 +7,7 @@ import 'package:localsend_app/provider/device_info_provider.dart'; import 'package:localsend_app/provider/network/send_provider.dart'; import 'package:localsend_app/util/sleep.dart'; import 'package:localsend_app/widget/animations/initial_fade_transition.dart'; +import 'package:localsend_app/widget/dialogs/error_dialog.dart'; import 'package:localsend_app/widget/list_tile/device_list_tile.dart'; import 'package:localsend_app/widget/responsive_list_view.dart'; import 'package:routerino/routerino.dart'; @@ -60,6 +61,7 @@ class _SendPageState extends ConsumerState { ); } final myDevice = ref.watch(deviceInfoProvider); + final waiting = sendState?.status == SessionStatus.waiting; return WillPopScope( onWillPop: () async { @@ -116,6 +118,34 @@ class _SendPageState extends ConsumerState { Padding( padding: const EdgeInsets.only(bottom: 20), child: Text(t.sendPage.rejected, style: const TextStyle(color: Colors.orange), textAlign: TextAlign.center), + ) + else if (sendState.status == SessionStatus.recipientBusy) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text(t.sendPage.busy, style: const TextStyle(color: Colors.orange), textAlign: TextAlign.center), + ) + else if (sendState.status == SessionStatus.finishedWithErrors) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(t.general.error, style: const TextStyle(color: Colors.orange)), + if (sendState.errorMessage != null) + TextButton( + style: TextButton.styleFrom( + foregroundColor: Colors.orange, + ), + onPressed: () { + showDialog( + context: context, + builder: (_) => ErrorDialog(error: sendState.errorMessage!), + ); + }, + child: const Icon(Icons.info), + ), + ], + ), ), Center( child: ElevatedButton.icon( @@ -123,8 +153,8 @@ class _SendPageState extends ConsumerState { _cancel(); context.pop(); }, - icon: Icon(sendState.status == SessionStatus.declined ? Icons.check_circle : Icons.close), - label: Text(sendState.status == SessionStatus.declined ? t.general.close : t.general.cancel), + icon: Icon(waiting ? Icons.close : Icons.check_circle), + label: Text(waiting ? t.general.cancel : t.general.close), ), ), ], diff --git a/lib/provider/network/send_provider.dart b/lib/provider/network/send_provider.dart index d1186591..b238fa77 100644 --- a/lib/provider/network/send_provider.dart +++ b/lib/provider/network/send_provider.dart @@ -70,6 +70,7 @@ class SendNotifier extends StateNotifier { startTime: null, endTime: null, cancelToken: cancelToken, + errorMessage: null, ); final originDevice = _ref.read(deviceInfoProvider); @@ -85,54 +86,64 @@ class SendNotifier extends StateNotifier { }, ); + state = requestState; + + // ignore: use_build_context_synchronously + Routerino.context.push(() => const SendPage(), transition: RouterinoTransition.fade); + + final Response response; try { - state = requestState; - - // ignore: use_build_context_synchronously - Routerino.context.push(() => const SendPage(), transition: RouterinoTransition.fade); - - final response = await requestDio.post( + response = await requestDio.post( ApiRoute.sendRequest.target(target), data: requestDto.toJson(), cancelToken: cancelToken, ); - - final responseMap = response.data as Map; - if (responseMap.isEmpty) { - // receiver has nothing selected - - // ignore: use_build_context_synchronously - Routerino.context.pushRootImmediately(() => const HomePage(appStart: false)); - state = null; - return; + } catch (e) { + if (e is DioError && e.response?.statusCode == 403) { + state = state?.copyWith( + status: SessionStatus.declined, + ); + } else if (e is DioError && e.response?.statusCode == 409) { + state = state?.copyWith( + status: SessionStatus.recipientBusy, + ); + } else { + state = state?.copyWith( + status: SessionStatus.finishedWithErrors, + errorMessage: e.humanErrorMessage, + ); } + return; + } - final sendingFiles = { - for (final file in requestState.files.values) - file.file.id: - responseMap.containsKey(file.file.id) ? file.copyWith(token: responseMap[file.file.id]) : file.copyWith(status: FileStatus.skipped), - }; + final responseMap = response.data as Map; + if (responseMap.isEmpty) { + // receiver has nothing selected // ignore: use_build_context_synchronously - Routerino.context.pushAndRemoveUntilImmediately( - removeUntil: SendPage, - builder: () => const ProgressPage(), - ); - - state = requestState.copyWith( - status: SessionStatus.sending, - files: sendingFiles, - ); - - await _send(uploadDio, target, sendingFiles); - } on DioError catch (e) { - if (e.type != DioErrorType.response && e.type != DioErrorType.cancel) { - print(e); - } - state = state?.copyWith( - status: SessionStatus.declined, - ); + Routerino.context.pushRootImmediately(() => const HomePage(appStart: false)); + state = null; + return; } + + final sendingFiles = { + for (final file in requestState.files.values) + file.file.id: + responseMap.containsKey(file.file.id) ? file.copyWith(token: responseMap[file.file.id]) : file.copyWith(status: FileStatus.skipped), + }; + + // ignore: use_build_context_synchronously + Routerino.context.pushAndRemoveUntilImmediately( + removeUntil: SendPage, + builder: () => const ProgressPage(), + ); + + state = requestState.copyWith( + status: SessionStatus.sending, + files: sendingFiles, + ); + + await _send(uploadDio, target, sendingFiles); } Future _send(Dio dio, Device target, Map files) async { @@ -221,3 +232,21 @@ extension on SendState { ); } } + +extension on Object { + String get humanErrorMessage { + final e = this; + if (e is DioError && e.response != null) { + final body = e.response!.data; + String message; + try { + message = (body as Map)['message']; + } catch (_) { + message = body; + } + return '[${e.response!.statusCode}] $message'; + } + + return e.toString(); + } +} diff --git a/lib/widget/dialogs/error_dialog.dart b/lib/widget/dialogs/error_dialog.dart new file mode 100644 index 00000000..4bd4a2e6 --- /dev/null +++ b/lib/widget/dialogs/error_dialog.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:localsend_app/gen/strings.g.dart'; +import 'package:routerino/routerino.dart'; + +class ErrorDialog extends StatelessWidget { + final String error; + + const ErrorDialog({required this.error, super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(t.dialogs.errorDialog.title), + content: SelectableText(error), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text(t.general.close), + ) + ], + ); + } +}