6ef658558a
First slice of the tier 2 offline mode work tracked in Fable #147. Notes are the only domain wired so far; this PR establishes the shape and proves the round-trip end-to-end before extending to tasks / projects / events / etc. in phase 2. Local store - Adds drift ^2.20.0 + sqlite3_flutter_libs ^0.5.24 + path ^1.9.0 runtime deps and drift_dev / build_runner dev deps. - New lib/data/local/database.dart: FabledDatabase with CachedNotes (mirroring the Note model 1:1; tags stored as JSON-encoded text) and SyncMetadata (per-domain last_synced_at). Database file lives at $appDocs/fabled_cache.sqlite; opened lazily in a background isolate. - fabledDatabaseProvider on the existing api_client_provider; closes the DB when the ProviderScope disposes. Repository pattern - NotesRepository now takes (NotesApi, FabledDatabase). Reads attempt the network first; on success the response is written to the cache. On NetworkException reads fall back to the cache (rethrowing if it is empty so fresh-install offline still surfaces the network error). - Writes (create/update/delete) hit the server then sync the cache; offline write queueing is phase 3 and explicitly not wired here. - AuthStatus.offline stays owned by AuthNotifier.verify()'s heartbeat; this repo deliberately doesn't poke that state. OfflineBanner - Reads notesRepository.lastSyncedAt(); when present, swaps "Offline — showing cached data." for "Offline — last sync X min ago." A 30s timer refreshes the relative-time string while the banner is mounted. - Falls back to the generic message if no cache exists yet (fresh install offline). Verification - flutter analyze: No issues found Out of scope (later phases per the task body) - Tasks / projects / milestones / events / conversation list / journal day caching (phase 2 — same wrapper pattern, repeated) - Generic write queue with conflict resolution on updated_at (phase 3) - Read-only UI indicators on screens that depend on cached data (phase 4) - Full offline chat (out of scope; needs a local model) - RAG / search over cached content (out of scope) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
131 lines
4.1 KiB
Dart
131 lines
4.1 KiB
Dart
import 'package:cookie_jar/cookie_jar.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../data/api/api_client.dart';
|
|
import '../data/api/auth_api.dart';
|
|
import '../data/api/chat_api.dart';
|
|
import '../data/api/journal_api.dart';
|
|
import '../data/api/knowledge_api.dart';
|
|
import '../data/api/voice_api.dart';
|
|
import '../data/api/milestones_api.dart';
|
|
import '../data/api/events_api.dart';
|
|
import '../data/api/notes_api.dart';
|
|
import '../data/api/projects_api.dart';
|
|
import '../data/api/quick_capture_api.dart';
|
|
import '../data/api/settings_api.dart';
|
|
import '../data/api/tasks_api.dart';
|
|
import '../data/local/database.dart';
|
|
import '../data/repositories/auth_repository.dart';
|
|
import '../data/repositories/chat_repository.dart';
|
|
import '../data/repositories/knowledge_repository.dart';
|
|
import '../data/repositories/voice_repository.dart';
|
|
import '../data/repositories/milestones_repository.dart';
|
|
import '../data/repositories/notes_repository.dart';
|
|
import '../data/repositories/projects_repository.dart';
|
|
import '../data/repositories/tasks_repository.dart';
|
|
import 'settings_provider.dart';
|
|
|
|
final cookieJarProvider = Provider<PersistCookieJar>((ref) {
|
|
final cookiesPath = ref.watch(cookiesPathProvider);
|
|
return buildCookieJar(cookiesPath);
|
|
});
|
|
|
|
final dioProvider = Provider<Dio>((ref) {
|
|
final url = ref.watch(serverUrlProvider) ?? '';
|
|
final jar = ref.watch(cookieJarProvider);
|
|
return buildDio(url, jar);
|
|
});
|
|
|
|
final authApiProvider = Provider<AuthApi>((ref) {
|
|
return AuthApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final notesApiProvider = Provider<NotesApi>((ref) {
|
|
return NotesApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final tasksApiProvider = Provider<TasksApi>((ref) {
|
|
return TasksApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final chatApiProvider = Provider<ChatApi>((ref) {
|
|
return ChatApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final quickCaptureApiProvider = Provider<QuickCaptureApi>((ref) {
|
|
return QuickCaptureApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final projectsApiProvider = Provider<ProjectsApi>((ref) {
|
|
return ProjectsApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
/// Local SQLite cache used by repositories for read-through caching and
|
|
/// offline fallback. Backed by Drift; lives for the lifetime of the
|
|
/// ProviderScope and is closed automatically when the scope disposes.
|
|
final fabledDatabaseProvider = Provider<FabledDatabase>((ref) {
|
|
final db = FabledDatabase();
|
|
ref.onDispose(db.close);
|
|
return db;
|
|
});
|
|
|
|
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|
return AuthRepository(ref.watch(authApiProvider));
|
|
});
|
|
|
|
final notesRepositoryProvider = Provider<NotesRepository>((ref) {
|
|
return NotesRepository(
|
|
ref.watch(notesApiProvider),
|
|
ref.watch(fabledDatabaseProvider),
|
|
);
|
|
});
|
|
|
|
final tasksRepositoryProvider = Provider<TasksRepository>((ref) {
|
|
return TasksRepository(ref.watch(tasksApiProvider));
|
|
});
|
|
|
|
final chatRepositoryProvider = Provider<ChatRepository>((ref) {
|
|
return ChatRepository(ref.watch(chatApiProvider));
|
|
});
|
|
|
|
final projectsRepositoryProvider = Provider<ProjectsRepository>((ref) {
|
|
return ProjectsRepository(ref.watch(projectsApiProvider));
|
|
});
|
|
|
|
final milestonesApiProvider = Provider<MilestonesApi>((ref) {
|
|
return MilestonesApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final milestonesRepositoryProvider = Provider<MilestonesRepository>((ref) {
|
|
return MilestonesRepository(ref.watch(milestonesApiProvider));
|
|
});
|
|
|
|
final knowledgeApiProvider = Provider<KnowledgeApi>((ref) {
|
|
return KnowledgeApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final knowledgeRepositoryProvider = Provider<KnowledgeRepository>((ref) {
|
|
return KnowledgeRepository(ref.watch(knowledgeApiProvider));
|
|
});
|
|
|
|
final journalApiProvider = Provider<JournalApi>((ref) {
|
|
return JournalApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final settingsApiProvider = Provider<SettingsApi>((ref) {
|
|
return SettingsApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final voiceApiProvider = Provider<VoiceApi>((ref) {
|
|
return VoiceApi(ref.watch(dioProvider));
|
|
});
|
|
|
|
final voiceRepositoryProvider = Provider<VoiceRepository>((ref) {
|
|
return VoiceRepository(ref.watch(voiceApiProvider));
|
|
});
|
|
|
|
final eventsApiProvider = Provider<EventsApi>((ref) {
|
|
return EventsApi(ref.watch(dioProvider));
|
|
});
|