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
+81 -6
View File
@@ -1,14 +1,26 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../brick/cache_helpers.dart';
import '../models/profile.dart';
import '../utils/snackbar.dart' show isOfflineSaveError;
import 'auth_provider.dart';
import 'connectivity_provider.dart';
import 'supabase_provider.dart';
import 'stream_recovery.dart';
// ---------------------------------------------------------------------------
// Offline-pending state
// ---------------------------------------------------------------------------
/// Pending full-name updates made offline, keyed by userId.
final offlinePendingProfileUpdatesProvider =
StateProvider<Map<String, String>>((ref) => const {});
final currentUserIdProvider = Provider<String?>((ref) {
final authState = ref.watch(authStateChangesProvider);
// Be explicit about loading/error to avoid dynamic dispatch problems.
@@ -37,9 +49,52 @@ final currentProfileProvider = StreamProvider<Profile?>((ref) {
return data == null ? [] : [Profile.fromMap(data)];
},
fromMap: Profile.fromMap,
onOfflineData: () async {
final all = await cachedListFromBrick<Profile>();
return all.where((p) => p.id == userId).toList();
},
onCacheMirror: (rows) => mirrorBatchToBrick<Profile>(
rows.whereType<Profile>().toList(),
tag: 'profile_current',
),
);
ref.onDispose(wrapper.dispose);
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
if (wasOnline == true || !isNowOnline) return;
final pending = Map<String, String>.from(
ref.read(offlinePendingProfileUpdatesProvider),
);
if (pending.isEmpty) return;
debugPrint(
'[currentProfileProvider] reconnected — syncing ${pending.length} profile update(s)',
);
final synced = <String>[];
for (final entry in pending.entries) {
try {
await client
.from('profiles')
.update({'full_name': entry.value})
.eq('id', entry.key);
synced.add(entry.key);
} catch (e) {
debugPrint(
'[currentProfileProvider] failed to sync full_name for ${entry.key}: $e',
);
}
}
if (synced.isNotEmpty) {
final remaining = Map<String, String>.from(
ref.read(offlinePendingProfileUpdatesProvider),
);
for (final id in synced) {
remaining.remove(id);
}
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = remaining;
}
});
return wrapper.stream.map((result) {
return result.data.isEmpty ? null : result.data.first;
});
@@ -58,6 +113,13 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
return data.map(Profile.fromMap).toList();
},
fromMap: Profile.fromMap,
onOfflineData: () async {
final all = await cachedListFromBrick<Profile>();
all.sort((a, b) => a.fullName.compareTo(b.fullName));
return all;
},
onCacheMirror: (rows) =>
mirrorBatchToBrick<Profile>(rows, tag: 'profiles'),
);
ref.onDispose(wrapper.dispose);
@@ -67,23 +129,36 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
/// Controller for the current user's profile (update full name / password).
final profileControllerProvider = Provider<ProfileController>((ref) {
final client = ref.watch(supabaseClientProvider);
return ProfileController(client);
return ProfileController(client, ref);
});
class ProfileController {
ProfileController(this._client);
ProfileController(this._client, [this._ref]);
final SupabaseClient _client;
final Ref? _ref;
/// Update the `profiles.full_name` for the given user id.
/// Offline: queues the update for replay on reconnect.
Future<void> updateFullName({
required String userId,
required String fullName,
}) async {
await _client
.from('profiles')
.update({'full_name': fullName})
.eq('id', userId);
try {
await _client
.from('profiles')
.update({'full_name': fullName})
.eq('id', userId);
} catch (e) {
if (!isOfflineSaveError(e)) rethrow;
final ref = _ref;
if (ref == null) return;
final current = Map<String, String>.from(
ref.read(offlinePendingProfileUpdatesProvider),
);
current[userId] = fullName;
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = current;
}
}
/// Update the current user's password (works for OAuth users too).