Add search, offline queue, app icon, and UI polish
- 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>
This commit is contained in:
@@ -41,6 +41,30 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Delete note?'),
|
||||
content: Text(_titleController.text),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await ref.read(notesProvider.notifier).delete(widget.noteId!);
|
||||
if (mounted) context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final title = _titleController.text.trim();
|
||||
final body = _contentController.text;
|
||||
@@ -79,6 +103,12 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
appBar: AppBar(
|
||||
title: Text(widget.noteId == null ? 'New Note' : 'Edit Note'),
|
||||
actions: [
|
||||
if (widget.noteId != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Delete',
|
||||
onPressed: _delete,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(_preview ? Icons.edit : Icons.preview),
|
||||
tooltip: _preview ? 'Edit' : 'Preview',
|
||||
|
||||
@@ -5,22 +5,63 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../core/constants.dart';
|
||||
import '../../providers/notes_provider.dart';
|
||||
|
||||
class NotesListScreen extends ConsumerWidget {
|
||||
class NotesListScreen extends ConsumerStatefulWidget {
|
||||
const NotesListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<NotesListScreen> createState() => _NotesListScreenState();
|
||||
}
|
||||
|
||||
class _NotesListScreenState extends ConsumerState<NotesListScreen> {
|
||||
bool _showSearch = false;
|
||||
String _search = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notesAsync = ref.watch(notesProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Notes')),
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: _showSearch
|
||||
? TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search notes…',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: (v) =>
|
||||
setState(() => _search = v.trim().toLowerCase()),
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
if (_showSearch)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: 'Close search',
|
||||
onPressed: () => setState(() {
|
||||
_showSearch = false;
|
||||
_search = '';
|
||||
}),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: 'Search',
|
||||
onPressed: () => setState(() => _showSearch = true),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: notesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
error: (_, _) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Error: $e'),
|
||||
const Icon(Icons.cloud_off, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Could not load notes.'),
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => ref.invalidate(notesProvider),
|
||||
child: const Text('Retry'),
|
||||
@@ -29,21 +70,46 @@ class NotesListScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
data: (notes) {
|
||||
if (notes.isEmpty) {
|
||||
return const Center(child: Text('No notes yet. Tap + to create one.'));
|
||||
final filtered = _search.isEmpty
|
||||
? notes
|
||||
: notes
|
||||
.where((n) =>
|
||||
n.title.toLowerCase().contains(_search) ||
|
||||
n.body.toLowerCase().contains(_search))
|
||||
.toList();
|
||||
|
||||
if (filtered.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_search.isEmpty
|
||||
? 'No notes yet. Tap + to create one.'
|
||||
: 'No notes match "$_search".',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(notesProvider.future),
|
||||
child: ListView.separated(
|
||||
itemCount: notes.length,
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, i) {
|
||||
final note = notes[i];
|
||||
final note = filtered[i];
|
||||
final preview = note.body
|
||||
.split('\n')
|
||||
.firstWhere((l) => l.trim().isNotEmpty, orElse: () => '')
|
||||
.trim();
|
||||
return ListTile(
|
||||
title: Text(note.title),
|
||||
subtitle: Text(
|
||||
note.updatedAt.toLocal().toString().substring(0, 16),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
preview.isNotEmpty
|
||||
? preview
|
||||
: note.updatedAt.toLocal().toString().substring(0, 16),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () => context.push(
|
||||
Routes.noteDetail.replaceFirst(':id', '${note.id}'),
|
||||
|
||||
Reference in New Issue
Block a user