mirror of
https://github.com/localsend/localsend.git
synced 2026-04-20 23:18:03 -04:00
feat: show error message in send page
This commit is contained in:
@@ -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:",
|
||||
|
||||
@@ -15,5 +15,6 @@ class SendState with _$SendState {
|
||||
required int? startTime,
|
||||
required int? endTime,
|
||||
required CancelToken? cancelToken,
|
||||
required String? errorMessage,
|
||||
}) = _SendState;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
23
lib/widget/dialogs/error_dialog.dart
Normal file
23
lib/widget/dialogs/error_dialog.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user