feat: show error message in send page

This commit is contained in:
Tien Do Nam
2023-02-08 21:32:57 +01:00
parent 824c1b092c
commit dba2fecb2d
6 changed files with 129 additions and 41 deletions

View File

@@ -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:",

View File

@@ -15,5 +15,6 @@ class SendState with _$SendState {
required int? startTime,
required int? endTime,
required CancelToken? cancelToken,
required String? errorMessage,
}) = _SendState;
}

View File

@@ -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)

View File

@@ -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<SendPage> {
);
}
final myDevice = ref.watch(deviceInfoProvider);
final waiting = sendState?.status == SessionStatus.waiting;
return WillPopScope(
onWillPop: () async {
@@ -116,6 +118,34 @@ class _SendPageState extends ConsumerState<SendPage> {
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<SendPage> {
_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),
),
),
],

View File

@@ -70,6 +70,7 @@ class SendNotifier extends StateNotifier<SendState?> {
startTime: null,
endTime: null,
cancelToken: cancelToken,
errorMessage: null,
);
final originDevice = _ref.read(deviceInfoProvider);
@@ -85,54 +86,64 @@ class SendNotifier extends StateNotifier<SendState?> {
},
);
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<void> _send(Dio dio, Device target, Map<String, SendingFile> 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();
}
}

View File

@@ -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),
)
],
);
}
}