This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/lib/providers/api_client_provider.dart
T
bvandeusen 76aff4ea9e feat(offline): tier 2 phase 4 — per-row pending-sync indicators
A small cloud-upload glyph appears next to any row whose id has a
queued offline write (offline-created temp ids and pending edits both
count). Tooltip reads "Pending sync — will save when online". Renders
nothing during normal online operation so the list stays clean.

- DB: `watchPendingIds(domain)` streams the union of target_id and
  temp_id across the queue, scoped per domain.
- Per-domain Riverpod stream providers for notes / tasks / projects.
- New `PendingSyncBadge` widget — used by KnowledgeItemCard (both
  list and grid variants), `_ProjectCard`, and `_TaskRow` in the
  project workspace.

flutter analyze clean; 21 tests pass. Closes #147 — all four phases
of Tier 2 offline mode are in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:14:36 -04:00

193 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/events_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 '../data/repositories/write_queue.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),
ref.watch(fabledDatabaseProvider),
);
});
final chatRepositoryProvider = Provider<ChatRepository>((ref) {
return ChatRepository(
ref.watch(chatApiProvider),
ref.watch(fabledDatabaseProvider),
);
});
final projectsRepositoryProvider = Provider<ProjectsRepository>((ref) {
return ProjectsRepository(
ref.watch(projectsApiProvider),
ref.watch(fabledDatabaseProvider),
);
});
final milestonesApiProvider = Provider<MilestonesApi>((ref) {
return MilestonesApi(ref.watch(dioProvider));
});
final milestonesRepositoryProvider = Provider<MilestonesRepository>((ref) {
return MilestonesRepository(
ref.watch(milestonesApiProvider),
ref.watch(fabledDatabaseProvider),
);
});
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));
});
final eventsRepositoryProvider = Provider<EventsRepository>((ref) {
return EventsRepository(
ref.watch(eventsApiProvider),
ref.watch(fabledDatabaseProvider),
);
});
/// Phase 3 — drains the offline write queue. Listen on
/// `writeQueueFailuresProvider` to surface dropped ops to the user;
/// `writeQueueDepthProvider` powers the OfflineBanner's "Retry (N)" affordance.
final writeQueueProvider = Provider<WriteQueue>((ref) {
final queue = WriteQueue(
db: ref.watch(fabledDatabaseProvider),
notesApi: ref.watch(notesApiProvider),
tasksApi: ref.watch(tasksApiProvider),
projectsApi: ref.watch(projectsApiProvider),
milestonesApi: ref.watch(milestonesApiProvider),
eventsApi: ref.watch(eventsApiProvider),
);
ref.onDispose(queue.dispose);
return queue;
});
final writeQueueDepthProvider = StreamProvider<int>((ref) {
return ref.watch(fabledDatabaseProvider).watchQueueDepth();
});
final writeQueueFailuresProvider = StreamProvider<QueueFailure>((ref) {
return ref.watch(writeQueueProvider).failures;
});
/// Per-domain set of ids with a queued write (target_id temp_id). Phase 4
/// — used by `PendingSyncBadge` in list views to mark rows that are still
/// in flight.
final pendingNoteIdsProvider = StreamProvider<Set<int>>((ref) {
return ref.watch(fabledDatabaseProvider).watchPendingIds(kSyncDomainNotes);
});
final pendingTaskIdsProvider = StreamProvider<Set<int>>((ref) {
return ref.watch(fabledDatabaseProvider).watchPendingIds(kSyncDomainTasks);
});
final pendingProjectIdsProvider = StreamProvider<Set<int>>((ref) {
return ref
.watch(fabledDatabaseProvider)
.watchPendingIds(kSyncDomainProjects);
});