Connection sheet got API status

This commit is contained in:
2025-12-24 22:58:07 +08:00
parent 2474c7f97c
commit a266177628
3 changed files with 238 additions and 21 deletions

View File

@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -16,6 +17,32 @@ import 'package:island/talker.dart';
import 'config.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 imagePickerProvider = Provider((ref) => ImagePicker());
final userAgentProvider = FutureProvider<String>((ref) async { final userAgentProvider = FutureProvider<String>((ref) async {
@@ -80,24 +107,58 @@ final apiClientProvider = Provider<Dio>((ref) {
dio.interceptors.addAll([ dio.interceptors.addAll([
InterceptorsWrapper( InterceptorsWrapper(
onRequest: ( onRequest:
RequestOptions options, (RequestOptions options, RequestInterceptorHandler handler) async {
RequestInterceptorHandler handler, try {
) async { final token = await getToken(ref.watch(tokenProvider));
try { if (token != null) {
final token = await getToken(ref.watch(tokenProvider)); options.headers['Authorization'] = 'AtField $token';
if (token != null) { }
options.headers['Authorization'] = 'AtField $token'; } catch (err) {
} // ignore
} catch (err) { }
// ignore
}
final userAgent = ref.read(userAgentProvider); final userAgent = ref.read(userAgentProvider);
if (userAgent.value != null) { if (userAgent.value != null) {
options.headers['User-Agent'] = userAgent.value; 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( TalkerDioLogger(

64
lib/pods/network.g.dart Normal file
View File

@@ -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<NetworkStatusNotifier, NetworkStatus> {
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<NetworkStatus>(value),
);
}
}
String _$networkStatusNotifierHash() =>
r'ca968c342be79cb97349fb95eee5c575d7076a99';
abstract class _$NetworkStatusNotifier extends $Notifier<NetworkStatus> {
NetworkStatus build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<NetworkStatus, NetworkStatus>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<NetworkStatus, NetworkStatus>,
NetworkStatus,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -2,10 +2,12 @@ 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/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';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
class NetworkStatusSheet extends HookConsumerWidget { class NetworkStatusSheet extends HookConsumerWidget {
final bool autoClose; final bool autoClose;
@@ -15,13 +17,22 @@ class NetworkStatusSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
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 wsNotifier = ref.watch(websocketStateProvider.notifier); final wsNotifier = ref.watch(websocketStateProvider.notifier);
final checks = [
wsState == WebSocketState.connected(),
apiState == NetworkStatus.online,
];
useEffect(() { useEffect(() {
if (!autoClose) return; if (!autoClose) return;
final checks = [wsState == WebSocketState.connected()]; final checks = [
wsState == WebSocketState.connected(),
apiState == NetworkStatus.online,
];
if (!checks.any((e) => !e)) { if (!checks.any((e) => !e)) {
Future.delayed(Duration(seconds: 3), () { Future.delayed(Duration(seconds: 3), () {
if (context.mounted) Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
@@ -29,18 +40,63 @@ class NetworkStatusSheet extends HookConsumerWidget {
} }
return null; return null;
}, [wsState]); }, [wsState, apiState]);
return SheetScaffold( return SheetScaffold(
heightFactor: 0.6, heightFactor: 0.6,
titleText: wsState == WebSocketState.connected() titleText: !checks.any((e) => !e)
? 'Connection Status' ? 'Connection Status'
: 'Connection Issue', : 'Connection Issues',
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 4,
children: [ 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( Row(
spacing: 8, spacing: 8,
children: [ 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( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
spacing: 8, spacing: 8,