6232c7c99a
Plans 1-3 implemented: Plan 1 — Foundation - Add google_fonts ^6.2.1 - lib/core/theme.dart: slate-indigo ColorScheme (dark/light), Fraunces headings, GradientButton widget - lib/providers/capture_work_queue_provider.dart: in-memory sequential work queue; CaptureWorkQueueNotifier drains one item at a time; captureResultProvider feeds snackbars to UI - lib/app.dart: wire fabledDarkTheme/fabledLightTheme; replace blocking _QuickCaptureBar with queue-based implementation (progress bar, badge) Plan 2 — Navigation - 3-tab shell: Briefing · Library · Chat (was Notes · Tasks · Projects · Chat) - lib/screens/library/library_screen.dart: unified notes+tasks+projects list with filter pills, status sub-filter for tasks, live search, FAB - lib/widgets/library_item_card.dart: NoteLibraryCard, TaskLibraryCard (status cycle), ProjectLibraryCard - lib/screens/chat/conversations_tab_screen.dart: focused replacement for ConversationsListScreen - Delete 5 dead screens: notes_list, tasks_list, project_list, conversations_list, quick_capture Plan 3 — Briefing - lib/data/models/briefing_conversation.dart - lib/data/api/briefing_api.dart: getToday, getHistory, getMessages, triggerSlot - lib/providers/briefing_provider.dart: BriefingNotifier with sendReply (optimistic + SSE + poll, same pattern as MessagesNotifier) and refresh - lib/widgets/chat_message_bubble.dart: extracted + redesigned shared bubble (ghost user bubbles, left-accent assistant bubbles) - lib/widgets/briefing_digest_card.dart: expandable first-message card - lib/screens/briefing/briefing_screen.dart: digest card, conversation list, streaming reply bar, refresh button, history overflow menu - lib/screens/briefing/briefing_history_screen.dart: read-only past dates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.4 KiB
Dart
99 lines
3.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../core/exceptions.dart';
|
|
import 'api_client_provider.dart';
|
|
import 'capture_queue_provider.dart';
|
|
import 'notes_provider.dart';
|
|
import 'tasks_provider.dart';
|
|
|
|
/// Outcome of a single capture attempt — consumed by the UI for snackbars.
|
|
class CaptureResult {
|
|
final String message;
|
|
final bool isError;
|
|
const CaptureResult(this.message, {this.isError = false});
|
|
}
|
|
|
|
/// The most recent capture result. UI watches this to show snackbars.
|
|
/// Reset to null by the notifier before each new item so listeners always fire.
|
|
final captureResultProvider = StateProvider<CaptureResult?>((_) => null);
|
|
|
|
/// In-memory sequential work queue for quick captures.
|
|
/// Separate from [captureQueueProvider] (which is the offline persistence queue).
|
|
final captureWorkQueueProvider =
|
|
StateNotifierProvider<CaptureWorkQueueNotifier, List<String>>(
|
|
(ref) => CaptureWorkQueueNotifier(ref),
|
|
);
|
|
|
|
class CaptureWorkQueueNotifier extends StateNotifier<List<String>> {
|
|
final Ref _ref;
|
|
bool _running = false;
|
|
|
|
CaptureWorkQueueNotifier(this._ref) : super([]);
|
|
|
|
/// Add text to the queue and start the drain loop if not already running.
|
|
void enqueue(String text) {
|
|
state = [...state, text];
|
|
_drain();
|
|
}
|
|
|
|
Future<void> _drain() async {
|
|
if (_running) return;
|
|
_running = true;
|
|
try {
|
|
while (state.isNotEmpty) {
|
|
final text = state.first;
|
|
// Signal "no result yet" so the same result value can re-trigger watch.
|
|
_ref.read(captureResultProvider.notifier).state = null;
|
|
try {
|
|
final api = _ref.read(quickCaptureApiProvider);
|
|
final result = await api.capture(text);
|
|
|
|
// Dequeue on success.
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
|
|
// Invalidate content providers so lists refresh.
|
|
switch (result.type) {
|
|
case 'note':
|
|
_ref.invalidate(notesProvider);
|
|
case 'task':
|
|
case 'todo':
|
|
_ref.invalidate(tasksProvider);
|
|
}
|
|
|
|
// Publish result for snackbar.
|
|
final msg = result.message.isNotEmpty
|
|
? result.message
|
|
: '${_typeLabel(result.type)} created: ${result.title}';
|
|
_ref.read(captureResultProvider.notifier).state = CaptureResult(msg);
|
|
} on NetworkException catch (_) {
|
|
// Persist to offline queue and stop draining — still offline.
|
|
await _ref.read(captureQueueProvider.notifier).enqueue(text);
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
_ref.read(captureResultProvider.notifier).state = CaptureResult(
|
|
"You're offline — capture saved and will retry automatically.",
|
|
);
|
|
break;
|
|
} on AppException catch (e) {
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
_ref.read(captureResultProvider.notifier).state =
|
|
CaptureResult(e.message, isError: true);
|
|
} catch (_) {
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
_ref.read(captureResultProvider.notifier).state =
|
|
CaptureResult('Capture failed. Please try again.', isError: true);
|
|
}
|
|
}
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
|
|
String _typeLabel(String type) => switch (type) {
|
|
'note' => 'Note',
|
|
'task' => 'Task',
|
|
'event' => 'Event',
|
|
'todo' => 'To-do',
|
|
_ => type,
|
|
};
|
|
}
|