From a2661776282709b1ffcb5ed1725078ffdf345bda Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 24 Dec 2025 22:58:07 +0800 Subject: [PATCH] :sparkles: Connection sheet got API status --- lib/pods/network.dart | 93 +++++++++++++--- lib/pods/network.g.dart | 64 +++++++++++ lib/widgets/content/network_status_sheet.dart | 102 +++++++++++++++++- 3 files changed, 238 insertions(+), 21 deletions(-) create mode 100644 lib/pods/network.g.dart diff --git a/lib/pods/network.dart b/lib/pods/network.dart index a9cc3fba..9bf87559 100644 --- a/lib/pods/network.dart +++ b/lib/pods/network.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -16,6 +17,32 @@ import 'package:island/talker.dart'; import 'config.dart'; +part 'network.g.dart'; + +// Network status enum to track different states +enum NetworkStatus { online, maintenance, offline } + +// Provider for network status using Riverpod v3 annotation +@riverpod +class NetworkStatusNotifier extends _$NetworkStatusNotifier { + @override + NetworkStatus build() { + return NetworkStatus.online; + } + + void setOnline() { + state = NetworkStatus.online; + } + + void setMaintenance() { + state = NetworkStatus.maintenance; + } + + void setOffline() { + state = NetworkStatus.offline; + } +} + final imagePickerProvider = Provider((ref) => ImagePicker()); final userAgentProvider = FutureProvider((ref) async { @@ -80,24 +107,58 @@ final apiClientProvider = Provider((ref) { dio.interceptors.addAll([ InterceptorsWrapper( - onRequest: ( - RequestOptions options, - RequestInterceptorHandler handler, - ) async { - try { - final token = await getToken(ref.watch(tokenProvider)); - if (token != null) { - options.headers['Authorization'] = 'AtField $token'; - } - } catch (err) { - // ignore - } + onRequest: + (RequestOptions options, RequestInterceptorHandler handler) async { + try { + final token = await getToken(ref.watch(tokenProvider)); + if (token != null) { + options.headers['Authorization'] = 'AtField $token'; + } + } catch (err) { + // ignore + } - final userAgent = ref.read(userAgentProvider); - if (userAgent.value != null) { - options.headers['User-Agent'] = userAgent.value; + final userAgent = ref.read(userAgentProvider); + if (userAgent.value != null) { + options.headers['User-Agent'] = userAgent.value; + } + return handler.next(options); + }, + onResponse: (response, handler) { + // Check for 503 status code (Service Unavailable/Maintenance) + if (response.statusCode == 503) { + final networkStatusNotifier = ref.read( + networkStatusProvider.notifier, + ); + networkStatusNotifier.setMaintenance(); + } else if (response.statusCode != null && + response.statusCode! >= 200 && + response.statusCode! < 300) { + // Set online status for successful responses + final networkStatusNotifier = ref.read( + networkStatusProvider.notifier, + ); + networkStatusNotifier.setOnline(); } - return handler.next(options); + return handler.next(response); + }, + onError: (error, handler) { + // Handle network errors and set offline status + if (error.type == DioExceptionType.connectionTimeout || + error.type == DioExceptionType.receiveTimeout || + error.type == DioExceptionType.sendTimeout || + error.type == DioExceptionType.connectionError) { + final networkStatusNotifier = ref.read( + networkStatusProvider.notifier, + ); + networkStatusNotifier.setOffline(); + } else if (error.response?.statusCode == 503) { + final networkStatusNotifier = ref.read( + networkStatusProvider.notifier, + ); + networkStatusNotifier.setMaintenance(); + } + return handler.next(error); }, ), TalkerDioLogger( diff --git a/lib/pods/network.g.dart b/lib/pods/network.g.dart new file mode 100644 index 00000000..0e53ba6c --- /dev/null +++ b/lib/pods/network.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'network.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(NetworkStatusNotifier) +const networkStatusProvider = NetworkStatusNotifierProvider._(); + +final class NetworkStatusNotifierProvider + extends $NotifierProvider { + const NetworkStatusNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'networkStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$networkStatusNotifierHash(); + + @$internal + @override + NetworkStatusNotifier create() => NetworkStatusNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(NetworkStatus value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$networkStatusNotifierHash() => + r'ca968c342be79cb97349fb95eee5c575d7076a99'; + +abstract class _$NetworkStatusNotifier extends $Notifier { + NetworkStatus build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + NetworkStatus, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/widgets/content/network_status_sheet.dart b/lib/widgets/content/network_status_sheet.dart index 54f5ddde..e67c76b2 100644 --- a/lib/widgets/content/network_status_sheet.dart +++ b/lib/widgets/content/network_status_sheet.dart @@ -2,10 +2,12 @@ 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/network.dart'; import 'package:island/pods/websocket.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class NetworkStatusSheet extends HookConsumerWidget { final bool autoClose; @@ -15,13 +17,22 @@ class NetworkStatusSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ws = ref.watch(websocketProvider); final wsState = ref.watch(websocketStateProvider); + final apiState = ref.watch(networkStatusProvider); final wsNotifier = ref.watch(websocketStateProvider.notifier); + final checks = [ + wsState == WebSocketState.connected(), + apiState == NetworkStatus.online, + ]; + useEffect(() { if (!autoClose) return; - final checks = [wsState == WebSocketState.connected()]; + final checks = [ + wsState == WebSocketState.connected(), + apiState == NetworkStatus.online, + ]; if (!checks.any((e) => !e)) { Future.delayed(Duration(seconds: 3), () { if (context.mounted) Navigator.of(context).pop(); @@ -29,18 +40,63 @@ class NetworkStatusSheet extends HookConsumerWidget { } return null; - }, [wsState]); + }, [wsState, apiState]); return SheetScaffold( heightFactor: 0.6, - titleText: wsState == WebSocketState.connected() + titleText: !checks.any((e) => !e) ? 'Connection Status' - : 'Connection Issue', + : 'Connection Issues', child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 4, children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: !checks.any((e) => !e) + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + margin: const EdgeInsets.only(bottom: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('overview').tr().bold(), + Column( + spacing: 8, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!checks.any((e) => !e)) + Text('Everything is operational.'), + if (!checks[0]) + Text( + 'WebSocket is disconnected. Realtime updates are not available. You can try tap the reconnect button below to try connect again.', + ), + if (!checks[1]) + ...([ + Text( + 'API is unreachable, you can try again later. If the issue persists, please contact support. Or you can check the service status.', + ), + InkWell( + onTap: () { + launchUrlString("https://status.solsynth.dev"); + }, + child: Text( + 'Check Service Status', + ).textColor(Colors.blueAccent).bold(), + ), + ]), + ], + ), + ], + ), + ), Row( spacing: 8, children: [ @@ -100,6 +156,42 @@ class NetworkStatusSheet extends HookConsumerWidget { ), ], ), + Row( + spacing: 8, + children: [ + Text('API').bold(), + Text( + apiState == NetworkStatus.online + ? 'Online' + : apiState == NetworkStatus.maintenance + ? 'Under Maintenance' + : 'Offline', + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: apiState == NetworkStatus.online + ? Icon( + Symbols.check_circle, + key: ValueKey(NetworkStatus.online), + color: Colors.green, + size: 16, + ) + : apiState == NetworkStatus.maintenance + ? Icon( + Symbols.construction, + key: ValueKey(NetworkStatus.maintenance), + color: Colors.orange, + size: 16, + ) + : Icon( + Symbols.cloud_off, + key: ValueKey(NetworkStatus.offline), + color: Colors.red, + size: 16, + ), + ), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.end, spacing: 8,