API network request status

This commit is contained in:
2025-12-24 23:08:27 +08:00
parent a266177628
commit 82cb8c7ff9
4 changed files with 101 additions and 80 deletions

View File

@@ -20,7 +20,7 @@ import 'config.dart';
part 'network.g.dart'; part 'network.g.dart';
// Network status enum to track different states // Network status enum to track different states
enum NetworkStatus { online, maintenance, offline } enum NetworkStatus { online, notReady, maintenance, offline }
// Provider for network status using Riverpod v3 annotation // Provider for network status using Riverpod v3 annotation
@riverpod @riverpod
@@ -41,6 +41,10 @@ class NetworkStatusNotifier extends _$NetworkStatusNotifier {
void setOffline() { void setOffline() {
state = NetworkStatus.offline; state = NetworkStatus.offline;
} }
void setNotReady() {
state = NetworkStatus.notReady;
}
} }
final imagePickerProvider = Provider((ref) => ImagePicker()); final imagePickerProvider = Provider((ref) => ImagePicker());
@@ -130,7 +134,11 @@ final apiClientProvider = Provider<Dio>((ref) {
final networkStatusNotifier = ref.read( final networkStatusNotifier = ref.read(
networkStatusProvider.notifier, networkStatusProvider.notifier,
); );
networkStatusNotifier.setMaintenance(); if (response.headers.value('X-NotReady') != null) {
networkStatusNotifier.setNotReady();
} else {
networkStatusNotifier.setMaintenance();
}
} else if (response.statusCode != null && } else if (response.statusCode != null &&
response.statusCode! >= 200 && response.statusCode! >= 200 &&
response.statusCode! < 300) { response.statusCode! < 300) {
@@ -156,7 +164,11 @@ final apiClientProvider = Provider<Dio>((ref) {
final networkStatusNotifier = ref.read( final networkStatusNotifier = ref.read(
networkStatusProvider.notifier, networkStatusProvider.notifier,
); );
networkStatusNotifier.setMaintenance(); if (error.response?.headers.value('X-NotReady') != null) {
networkStatusNotifier.setNotReady();
} else {
networkStatusNotifier.setMaintenance();
}
} }
return handler.next(error); return handler.next(error);
}, },

View File

@@ -36,71 +36,41 @@ class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
} }
return user; return user;
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (!kIsWeb) { if (error is DioException) {
if (error is DioException) { if (error.response?.statusCode == 503) return null;
showOverlayDialog<bool>( showOverlayDialog<bool>(
builder: builder: (context, close) => AlertDialog(
(context, close) => AlertDialog( title: Text('failedToLoadUserInfo'.tr()),
title: Text('failedToLoadUserInfo'.tr()), content: Text(
content: Text( [
[ (error.response?.statusCode == 401
(error.response?.statusCode == 401 ? 'failedToLoadUserInfoUnauthorized'
? 'failedToLoadUserInfoUnauthorized' : 'failedToLoadUserInfoNetwork')
: 'failedToLoadUserInfoNetwork') .tr()
.tr() .trim(),
.trim(), '',
'', '${error.response?.statusCode ?? 'Network Error'}',
'${error.response?.statusCode ?? 'Network Error'}', if (error.response?.headers != null) error.response?.headers,
if (error.response?.headers != null) if (error.response?.data != null)
error.response?.headers, jsonEncode(error.response?.data),
if (error.response?.data != null) ].join('\n'),
jsonEncode(error.response?.data), ),
].join('\n'), actions: [
), TextButton(
actions: [ onPressed: () => close(false),
TextButton( child: Text('okay'.tr()),
onPressed: () => close(false), ),
child: Text('okay'.tr()), TextButton(
), onPressed: () => close(true),
TextButton( child: Text('retry'.tr()),
onPressed: () => close(true), ),
child: Text('retry'.tr()), ],
), ),
], ).then((value) {
), if (value == true) {
).then((value) { ref.invalidateSelf();
if (value == true) { }
ref.invalidateSelf(); });
}
});
} else {
showOverlayDialog<bool>(
builder:
(context, close) => AlertDialog(
title: Text('failedToLoadUserInfo'.tr()),
content: Text(
[
'failedToLoadUserInfoNetwork'.tr(),
error.toString(),
].join('\n\n').trim(),
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text('okay'.tr()),
),
TextButton(
onPressed: () => close(true),
child: Text('retry'.tr()),
),
],
),
).then((value) {
if (value == true) {
ref.invalidateSelf();
}
});
}
} }
talker.error( talker.error(
"[UserInfo] Failed to fetch user info...", "[UserInfo] Failed to fetch user info...",

View File

@@ -34,22 +34,43 @@ class AppWrapper extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final networkStateShowing = useState(false); final networkStateShowing = useState(false);
final websocketState = ref.watch(websocketStateProvider); final websocketState = ref.watch(websocketStateProvider);
final apiState = ref.watch(networkStatusProvider);
final isShowSnow = useState(false); final isShowSnow = useState(false);
final isSnowGone = useState(false); final isSnowGone = useState(false);
// Handle network status modal // Handle network status modal
if (websocketState == WebSocketState.duplicateDevice() && useEffect(() {
!networkStateShowing.value) { bool triedOpen = false;
networkStateShowing.value = true; if (websocketState == WebSocketState.duplicateDevice() &&
WidgetsBinding.instance.addPostFrameCallback((_) { !networkStateShowing.value &&
showModalBottomSheet( !triedOpen) {
context: context, networkStateShowing.value = true;
isScrollControlled: true, WidgetsBinding.instance.addPostFrameCallback((_) {
isDismissible: false, showModalBottomSheet(
builder: (context) => NetworkStatusSheet(autoClose: true), context: context,
).then((_) => networkStateShowing.value = false); isScrollControlled: true,
}); isDismissible: false,
} builder: (context) => NetworkStatusSheet(autoClose: true),
).then((_) => networkStateShowing.value = false);
});
triedOpen = true;
}
if (apiState != NetworkStatus.online &&
!networkStateShowing.value &&
!triedOpen) {
networkStateShowing.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const NetworkStatusSheet(),
).then((_) => networkStateShowing.value = false);
});
triedOpen = true;
}
return null;
}, [websocketState, apiState]);
// Initialize services and listeners // Initialize services and listeners
useEffect(() { useEffect(() {

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -18,6 +19,7 @@ class NetworkStatusSheet extends HookConsumerWidget {
final ws = ref.watch(websocketProvider); final ws = ref.watch(websocketProvider);
final wsState = ref.watch(websocketStateProvider); final wsState = ref.watch(websocketStateProvider);
final apiState = ref.watch(networkStatusProvider); final apiState = ref.watch(networkStatusProvider);
final serverUrl = ref.watch(serverUrlProvider);
final wsNotifier = ref.watch(websocketStateProvider.notifier); final wsNotifier = ref.watch(websocketStateProvider.notifier);
@@ -163,6 +165,8 @@ class NetworkStatusSheet extends HookConsumerWidget {
Text( Text(
apiState == NetworkStatus.online apiState == NetworkStatus.online
? 'Online' ? 'Online'
: apiState == NetworkStatus.notReady
? 'Not Ready'
: apiState == NetworkStatus.maintenance : apiState == NetworkStatus.maintenance
? 'Under Maintenance' ? 'Under Maintenance'
: 'Offline', : 'Offline',
@@ -176,6 +180,13 @@ class NetworkStatusSheet extends HookConsumerWidget {
color: Colors.green, color: Colors.green,
size: 16, size: 16,
) )
: apiState == NetworkStatus.notReady
? Icon(
Symbols.warning,
key: ValueKey(NetworkStatus.notReady),
color: Colors.orange,
size: 16,
)
: apiState == NetworkStatus.maintenance : apiState == NetworkStatus.maintenance
? Icon( ? Icon(
Symbols.construction, Symbols.construction,
@@ -192,6 +203,13 @@ class NetworkStatusSheet extends HookConsumerWidget {
), ),
], ],
), ),
Row(
spacing: 8,
children: [
Text('API Server').bold(),
Expanded(child: Text(serverUrl)),
],
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
spacing: 8, spacing: 8,