feat: make core pages local-first offline
This commit is contained in:
@@ -4,6 +4,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:miaoji_zhang/shared/api/backend_identity.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
|
||||
enum BackendAvailability { unknown, online, offline }
|
||||
|
||||
class ApiClient {
|
||||
ApiClient._();
|
||||
static final ApiClient instance = ApiClient._();
|
||||
@@ -12,6 +14,9 @@ class ApiClient {
|
||||
static const _legacyTokenKey = 'auth_token';
|
||||
static final _tokenKey = 'auth_token_${BackendIdentity.scope}';
|
||||
static final sessionExpired = ValueNotifier<int>(0);
|
||||
static final availability = ValueNotifier<BackendAvailability>(
|
||||
BackendAvailability.unknown,
|
||||
);
|
||||
static bool _handlingUnauthorized = false;
|
||||
|
||||
static const String baseUrl = BackendIdentity.baseUrl;
|
||||
@@ -31,6 +36,10 @@ class ApiClient {
|
||||
)
|
||||
..interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
availability.value = BackendAvailability.online;
|
||||
handler.next(response);
|
||||
},
|
||||
onRequest: (options, handler) async {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
if (token != null) {
|
||||
@@ -39,6 +48,11 @@ class ApiClient {
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
if (isConnectivityError(error)) {
|
||||
availability.value = BackendAvailability.offline;
|
||||
} else if (error.response != null) {
|
||||
availability.value = BackendAvailability.online;
|
||||
}
|
||||
final unauthorized = error.response?.statusCode == 401;
|
||||
final data = error.response?.data;
|
||||
final aiDenied =
|
||||
@@ -74,6 +88,18 @@ class ApiClient {
|
||||
}
|
||||
|
||||
Future<String?> readToken() => _storage.read(key: _tokenKey);
|
||||
|
||||
Future<void> probe() async {
|
||||
try {
|
||||
await dio.get<void>(
|
||||
'/api/public/brand',
|
||||
options: Options(receiveTimeout: const Duration(seconds: 10)),
|
||||
);
|
||||
} catch (_) {
|
||||
// The interceptor owns the reachability state transition.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearToken() async {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
await _storage.delete(key: _legacyTokenKey);
|
||||
@@ -91,8 +117,9 @@ String apiErrorMessage(Object e) {
|
||||
if (e is StateError) return e.message;
|
||||
if (e is DioException) {
|
||||
final data = e.response?.data;
|
||||
if (data is Map && data['message'] != null)
|
||||
if (data is Map && data['message'] != null) {
|
||||
return data['message'] as String;
|
||||
}
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
|
||||
Reference in New Issue
Block a user