Offline Support

This commit is contained in:
2026-04-27 06:20:39 +08:00
parent 7d8851a94a
commit 48cd3bcd60
121 changed files with 14798 additions and 2214 deletions
+11 -44
View File
@@ -1,19 +1,13 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'stream_recovery.dart';
import 'supabase_provider.dart';
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
ref,
) {
final client = ref.watch(supabaseClientProvider);
// ChangeNotifierProvider automatically disposes the notifier; no need
// for ref.onDispose here — adding it causes a double-dispose assertion.
return RealtimeController(client);
return RealtimeController();
});
/// Per-channel realtime controller for UI skeleton indicators.
@@ -27,42 +21,12 @@ final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
/// - Per-channel recovering state for pinpoint skeleton indicators
/// - Auth token refreshes for realtime connections
class RealtimeController extends ChangeNotifier {
final SupabaseClient _client;
bool _disposed = false;
/// Channels currently in a recovering/polling/stale state.
final Set<String> _recoveringChannels = {};
StreamSubscription<AuthState>? _authSub;
RealtimeController(this._client) {
_init();
}
void _init() {
try {
_authSub = _client.auth.onAuthStateChange.listen((data) {
final event = data.event;
if (event == AuthChangeEvent.tokenRefreshed) {
_ensureTokenFresh();
}
});
} catch (e) {
debugPrint('RealtimeController._init error: $e');
}
}
Future<void> _ensureTokenFresh() async {
if (_disposed) return;
try {
final authDynamic = _client.auth as dynamic;
if (authDynamic.refreshSession != null) {
await authDynamic.refreshSession?.call();
}
} catch (e) {
debugPrint('RealtimeController: token refresh failed: $e');
}
}
RealtimeController();
// ── Per-channel status ─────────────────────────────────────────────────
@@ -97,13 +61,17 @@ class RealtimeController extends ChangeNotifier {
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
///
/// Routes [StreamConnectionStatus] to the appropriate mark method.
/// Both `connected` and `polling` are treated as "recovered" because
/// polling is a functional fallback that still delivers data — the user
/// doesn't need to see a reconnection indicator while data flows via REST.
/// Routes [StreamConnectionStatus] to the appropriate mark method:
/// - `connected` — live stream; no indicator needed.
/// - `polling` — REST fallback; data is fresh, no skeleton needed.
/// - `offline` — device has no internet; Brick cache is active. The offline
/// banner already communicates this state, so the reconnection skeleton
/// should NOT show — it would be misleading while real data is on screen.
/// - Everything else (`recovering`, `stale`, `failed`) — show skeleton.
void handleChannelStatus(String channel, StreamConnectionStatus status) {
if (status == StreamConnectionStatus.connected ||
status == StreamConnectionStatus.polling) {
status == StreamConnectionStatus.polling ||
status == StreamConnectionStatus.offline) {
markChannelRecovered(channel);
} else {
markChannelRecovering(channel);
@@ -126,7 +94,6 @@ class RealtimeController extends ChangeNotifier {
@override
void dispose() {
_disposed = true;
_authSub?.cancel();
super.dispose();
}
}