Android app: Calendar, News, Knowledge, Voice, Nav restructure, bug fixes
This commit was merged in pull request #14.
This commit is contained in:
+7
-17
@@ -5,10 +5,10 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_timezone/flutter_timezone.dart';
|
||||
|
||||
import 'core/constants.dart';
|
||||
import 'core/exceptions.dart';
|
||||
import 'core/theme.dart';
|
||||
import 'providers/api_client_provider.dart';
|
||||
import 'providers/auth_provider.dart';
|
||||
import 'core/exceptions.dart';
|
||||
import 'providers/capture_queue_provider.dart';
|
||||
import 'providers/capture_work_queue_provider.dart';
|
||||
import 'providers/briefing_provider.dart';
|
||||
@@ -16,10 +16,8 @@ import 'providers/calendar_provider.dart';
|
||||
import 'providers/chat_provider.dart';
|
||||
import 'providers/knowledge_provider.dart';
|
||||
import 'providers/news_provider.dart';
|
||||
import 'providers/notes_provider.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'providers/update_provider.dart';
|
||||
import 'providers/tasks_provider.dart';
|
||||
import 'screens/auth/login_screen.dart';
|
||||
import 'screens/briefing/briefing_screen.dart';
|
||||
import 'screens/knowledge/knowledge_screen.dart';
|
||||
@@ -541,27 +539,19 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
|
||||
if (!mounted) return;
|
||||
final queue = ref.read(captureQueueProvider);
|
||||
if (queue.isEmpty) return;
|
||||
final api = ref.read(quickCaptureApiProvider);
|
||||
for (final text in List<String>.from(queue)) {
|
||||
if (!mounted) break;
|
||||
try {
|
||||
final result = await api.capture(text);
|
||||
// Dequeue before the mounted check — SharedPreferences doesn't need
|
||||
// the widget alive, and skipping this would leave a ghost item.
|
||||
final conv =
|
||||
await ref.read(conversationsProvider.notifier).create('');
|
||||
final chatRepo = ref.read(chatRepositoryProvider);
|
||||
await chatRepo.sendMessage(conv.id, text);
|
||||
chatRepo.streamGeneration(conv.id).drain<void>().ignore();
|
||||
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||
if (!mounted) break;
|
||||
switch (result.type) {
|
||||
case 'note':
|
||||
ref.invalidate(notesProvider);
|
||||
case 'task':
|
||||
case 'todo':
|
||||
ref.invalidate(tasksProvider);
|
||||
}
|
||||
} on NetworkException {
|
||||
break;
|
||||
} catch (_) {
|
||||
// Server error or unexpected failure — drop from queue to prevent
|
||||
// ghost items that can never be cleared.
|
||||
// Server error — drop from queue to prevent ghost items.
|
||||
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ 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';
|
||||
import 'chat_provider.dart';
|
||||
|
||||
/// Outcome of a single capture attempt — consumed by the UI for snackbars.
|
||||
class CaptureResult {
|
||||
@@ -25,7 +24,6 @@ class _CaptureResultNotifier extends Notifier<CaptureResult?> {
|
||||
}
|
||||
|
||||
/// In-memory sequential work queue for quick captures.
|
||||
/// Separate from [captureQueueProvider] (which is the offline persistence queue).
|
||||
final captureWorkQueueProvider =
|
||||
NotifierProvider<CaptureWorkQueueNotifier, List<String>>(
|
||||
CaptureWorkQueueNotifier.new,
|
||||
@@ -52,31 +50,24 @@ class CaptureWorkQueueNotifier extends Notifier<List<String>> {
|
||||
// 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);
|
||||
// Create a new conversation, add it to the conversations list, then
|
||||
// send the message and kick off generation in the background.
|
||||
final conv =
|
||||
await ref.read(conversationsProvider.notifier).create('');
|
||||
final chatRepo = ref.read(chatRepositoryProvider);
|
||||
await chatRepo.sendMessage(conv.id, text);
|
||||
// Fire-and-forget: drain the SSE stream so the server generates a
|
||||
// response (creating notes/tasks/etc.) without blocking the UI.
|
||||
chatRepo.streamGeneration(conv.id).drain<void>().ignore();
|
||||
|
||||
// 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);
|
||||
ref.read(captureResultProvider.notifier).state =
|
||||
const CaptureResult('Sent to Fabled.');
|
||||
} 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(
|
||||
ref.read(captureResultProvider.notifier).state = const CaptureResult(
|
||||
"You're offline — capture saved and will retry automatically.",
|
||||
);
|
||||
break;
|
||||
@@ -86,20 +77,13 @@ class CaptureWorkQueueNotifier extends Notifier<List<String>> {
|
||||
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);
|
||||
ref.read(captureResultProvider.notifier).state = const CaptureResult(
|
||||
'Failed to send. Please try again.',
|
||||
isError: true);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
String _typeLabel(String type) => switch (type) {
|
||||
'note' => 'Note',
|
||||
'task' => 'Task',
|
||||
'event' => 'Event',
|
||||
'todo' => 'To-do',
|
||||
_ => type,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,19 +143,19 @@ class _EventFormSheetState extends ConsumerState<EventFormSheet> {
|
||||
Future<void> _delete() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Delete this event?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(
|
||||
'Delete',
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
style: TextStyle(
|
||||
color: Theme.of(dialogContext).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -99,15 +99,15 @@ class ConversationsTabScreen extends ConsumerWidget {
|
||||
BuildContext context, WidgetRef ref, int id, String title) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Delete conversation?'),
|
||||
content: Text('"$title" will be permanently deleted.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Delete')),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user