From 82cb8c7ff90b0358cddfeab3751c93a08c6aa03e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 24 Dec 2025 23:08:27 +0800 Subject: [PATCH] :sparkles: API network request status --- lib/pods/network.dart | 18 +++- lib/pods/userinfo.dart | 100 ++++++------------ lib/widgets/app_wrapper.dart | 45 +++++--- lib/widgets/content/network_status_sheet.dart | 18 ++++ 4 files changed, 101 insertions(+), 80 deletions(-) diff --git a/lib/pods/network.dart b/lib/pods/network.dart index 9bf87559..a25d984d 100644 --- a/lib/pods/network.dart +++ b/lib/pods/network.dart @@ -20,7 +20,7 @@ import 'config.dart'; part 'network.g.dart'; // 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 @riverpod @@ -41,6 +41,10 @@ class NetworkStatusNotifier extends _$NetworkStatusNotifier { void setOffline() { state = NetworkStatus.offline; } + + void setNotReady() { + state = NetworkStatus.notReady; + } } final imagePickerProvider = Provider((ref) => ImagePicker()); @@ -130,7 +134,11 @@ final apiClientProvider = Provider((ref) { final networkStatusNotifier = ref.read( networkStatusProvider.notifier, ); - networkStatusNotifier.setMaintenance(); + if (response.headers.value('X-NotReady') != null) { + networkStatusNotifier.setNotReady(); + } else { + networkStatusNotifier.setMaintenance(); + } } else if (response.statusCode != null && response.statusCode! >= 200 && response.statusCode! < 300) { @@ -156,7 +164,11 @@ final apiClientProvider = Provider((ref) { final networkStatusNotifier = ref.read( networkStatusProvider.notifier, ); - networkStatusNotifier.setMaintenance(); + if (error.response?.headers.value('X-NotReady') != null) { + networkStatusNotifier.setNotReady(); + } else { + networkStatusNotifier.setMaintenance(); + } } return handler.next(error); }, diff --git a/lib/pods/userinfo.dart b/lib/pods/userinfo.dart index 878b0509..519b56dd 100644 --- a/lib/pods/userinfo.dart +++ b/lib/pods/userinfo.dart @@ -36,71 +36,41 @@ class UserInfoNotifier extends AsyncNotifier { } return user; } catch (error, stackTrace) { - if (!kIsWeb) { - if (error is DioException) { - showOverlayDialog( - builder: - (context, close) => AlertDialog( - title: Text('failedToLoadUserInfo'.tr()), - content: Text( - [ - (error.response?.statusCode == 401 - ? 'failedToLoadUserInfoUnauthorized' - : 'failedToLoadUserInfoNetwork') - .tr() - .trim(), - '', - '${error.response?.statusCode ?? 'Network Error'}', - if (error.response?.headers != null) - error.response?.headers, - if (error.response?.data != null) - jsonEncode(error.response?.data), - ].join('\n'), - ), - actions: [ - TextButton( - onPressed: () => close(false), - child: Text('okay'.tr()), - ), - TextButton( - onPressed: () => close(true), - child: Text('retry'.tr()), - ), - ], - ), - ).then((value) { - if (value == true) { - ref.invalidateSelf(); - } - }); - } else { - showOverlayDialog( - 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(); - } - }); - } + if (error is DioException) { + if (error.response?.statusCode == 503) return null; + showOverlayDialog( + builder: (context, close) => AlertDialog( + title: Text('failedToLoadUserInfo'.tr()), + content: Text( + [ + (error.response?.statusCode == 401 + ? 'failedToLoadUserInfoUnauthorized' + : 'failedToLoadUserInfoNetwork') + .tr() + .trim(), + '', + '${error.response?.statusCode ?? 'Network Error'}', + if (error.response?.headers != null) error.response?.headers, + if (error.response?.data != null) + jsonEncode(error.response?.data), + ].join('\n'), + ), + 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( "[UserInfo] Failed to fetch user info...", diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index 5a6fe784..b0ee605b 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -34,22 +34,43 @@ class AppWrapper extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final networkStateShowing = useState(false); final websocketState = ref.watch(websocketStateProvider); + final apiState = ref.watch(networkStatusProvider); final isShowSnow = useState(false); final isSnowGone = useState(false); // Handle network status modal - if (websocketState == WebSocketState.duplicateDevice() && - !networkStateShowing.value) { - networkStateShowing.value = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: false, - builder: (context) => NetworkStatusSheet(autoClose: true), - ).then((_) => networkStateShowing.value = false); - }); - } + useEffect(() { + bool triedOpen = false; + if (websocketState == WebSocketState.duplicateDevice() && + !networkStateShowing.value && + !triedOpen) { + networkStateShowing.value = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + showModalBottomSheet( + context: context, + 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 useEffect(() { diff --git a/lib/widgets/content/network_status_sheet.dart b/lib/widgets/content/network_status_sheet.dart index e67c76b2..7c39d84f 100644 --- a/lib/widgets/content/network_status_sheet.dart +++ b/lib/widgets/content/network_status_sheet.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/websocket.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -18,6 +19,7 @@ class NetworkStatusSheet extends HookConsumerWidget { final ws = ref.watch(websocketProvider); final wsState = ref.watch(websocketStateProvider); final apiState = ref.watch(networkStatusProvider); + final serverUrl = ref.watch(serverUrlProvider); final wsNotifier = ref.watch(websocketStateProvider.notifier); @@ -163,6 +165,8 @@ class NetworkStatusSheet extends HookConsumerWidget { Text( apiState == NetworkStatus.online ? 'Online' + : apiState == NetworkStatus.notReady + ? 'Not Ready' : apiState == NetworkStatus.maintenance ? 'Under Maintenance' : 'Offline', @@ -176,6 +180,13 @@ class NetworkStatusSheet extends HookConsumerWidget { color: Colors.green, size: 16, ) + : apiState == NetworkStatus.notReady + ? Icon( + Symbols.warning, + key: ValueKey(NetworkStatus.notReady), + color: Colors.orange, + size: 16, + ) : apiState == NetworkStatus.maintenance ? Icon( Symbols.construction, @@ -192,6 +203,13 @@ class NetworkStatusSheet extends HookConsumerWidget { ), ], ), + Row( + spacing: 8, + children: [ + Text('API Server').bold(), + Expanded(child: Text(serverUrl)), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.end, spacing: 8,