2a35fe5532
- Collapsible search in notes and tasks: magnifying glass in AppBar expands to a text field inline; close button resets the filter - Offline capture queue: failed quick-captures (NetworkException) are persisted to SharedPreferences and retried automatically on next successful submit or app start; badge shows pending count - App icon: book-with-sparkle logo from FabledAssistant SVG rendered at all Android densities with adaptive icon (indigo #6366f1 bg) - Dark mode subtitle fix: use colorScheme.onSurfaceVariant for note preview and conversation timestamp text - Remove swipe-to-delete (accidental deletions); long-press remains - Chat: SSE streaming reliability, polling fallback, title patching - Settings: theme toggle (system/light/dark) persisted to prefs - Notes: delete button in edit screen; body preview in list - Tasks: fix delete dialog context; description search support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
4.7 KiB
Dart
152 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../core/exceptions.dart';
|
|
import '../../providers/api_client_provider.dart';
|
|
import '../../providers/notes_provider.dart';
|
|
import '../../providers/tasks_provider.dart';
|
|
|
|
class QuickCaptureScreen extends ConsumerStatefulWidget {
|
|
const QuickCaptureScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<QuickCaptureScreen> createState() =>
|
|
_QuickCaptureScreenState();
|
|
}
|
|
|
|
class _QuickCaptureScreenState extends ConsumerState<QuickCaptureScreen> {
|
|
final _controller = TextEditingController();
|
|
final _focusNode = FocusNode();
|
|
bool _loading = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _send() async {
|
|
final text = _controller.text.trim();
|
|
if (text.isEmpty) return;
|
|
setState(() => _loading = true);
|
|
try {
|
|
final result =
|
|
await ref.read(quickCaptureApiProvider).capture(text);
|
|
// Invalidate the relevant provider so the list screen re-fetches.
|
|
switch (result.type) {
|
|
case 'note':
|
|
ref.invalidate(notesProvider);
|
|
case 'task':
|
|
case 'todo':
|
|
ref.invalidate(tasksProvider);
|
|
}
|
|
|
|
if (mounted) {
|
|
// Use the server's human-readable message if available, else compose one.
|
|
final msg = result.message.isNotEmpty
|
|
? result.message
|
|
: '${_typeLabel(result.type)} created: ${result.title}';
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(msg),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
context.pop();
|
|
}
|
|
} on AppException catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(e.message),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
String _typeLabel(String type) => switch (type) {
|
|
'note' => 'Note',
|
|
'task' => 'Task',
|
|
'event' => 'Event',
|
|
'todo' => 'To-do',
|
|
_ => type,
|
|
};
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Quick Capture')),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'What\'s on your mind?',
|
|
style: theme.textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Describe a note, task, event, or research item — Fabled will figure out the rest.',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _controller,
|
|
focusNode: _focusNode,
|
|
autofocus: true,
|
|
enabled: !_loading,
|
|
maxLines: 6,
|
|
minLines: 3,
|
|
keyboardType: TextInputType.multiline,
|
|
decoration: InputDecoration(
|
|
hintText:
|
|
'e.g. "Remind me to call the dentist next Monday" or "Note: project meeting went well, key points were..."',
|
|
border: const OutlineInputBorder(),
|
|
alignLabelWithHint: true,
|
|
suffixIcon: _controller.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_controller.clear();
|
|
setState(() {});
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (_) => setState(() {}),
|
|
),
|
|
const SizedBox(height: 16),
|
|
FilledButton.icon(
|
|
onPressed: (_loading || _controller.text.trim().isEmpty)
|
|
? null
|
|
: _send,
|
|
icon: _loading
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Icon(Icons.auto_awesome),
|
|
label: Text(_loading ? 'Processing...' : 'Capture'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|