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:
@@ -85,14 +85,15 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
||||
Future<void> _delete() async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Delete task?'),
|
||||
content: Text(_titleController.text),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Delete')),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -16,6 +16,8 @@ class TasksListScreen extends ConsumerStatefulWidget {
|
||||
class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabs;
|
||||
bool _showSearch = false;
|
||||
String _search = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -35,7 +37,35 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Tasks'),
|
||||
automaticallyImplyLeading: false,
|
||||
title: _showSearch
|
||||
? TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search tasks…',
|
||||
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),
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabs,
|
||||
tabs: const [
|
||||
@@ -47,11 +77,14 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
),
|
||||
body: tasksAsync.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 tasks.'),
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => ref.invalidate(tasksProvider),
|
||||
child: const Text('Retry'),
|
||||
@@ -60,21 +93,29 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
),
|
||||
),
|
||||
data: (tasks) {
|
||||
final filtered = _search.isEmpty
|
||||
? tasks
|
||||
: tasks
|
||||
.where((t) =>
|
||||
t.title.toLowerCase().contains(_search) ||
|
||||
(t.description ?? '').toLowerCase().contains(_search))
|
||||
.toList();
|
||||
|
||||
final todo =
|
||||
tasks.where((t) => t.status == TaskStatus.todo).toList();
|
||||
filtered.where((t) => t.status == TaskStatus.todo).toList();
|
||||
final inProgress =
|
||||
tasks.where((t) => t.status == TaskStatus.inProgress).toList();
|
||||
filtered.where((t) => t.status == TaskStatus.inProgress).toList();
|
||||
final done =
|
||||
tasks.where((t) => t.status == TaskStatus.done).toList();
|
||||
filtered.where((t) => t.status == TaskStatus.done).toList();
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(tasksProvider.future),
|
||||
child: TabBarView(
|
||||
controller: _tabs,
|
||||
children: [
|
||||
_TaskList(tasks: todo),
|
||||
_TaskList(tasks: inProgress),
|
||||
_TaskList(tasks: done),
|
||||
_TaskList(tasks: todo, search: _search),
|
||||
_TaskList(tasks: inProgress, search: _search),
|
||||
_TaskList(tasks: done, search: _search),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -91,12 +132,17 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
|
||||
class _TaskList extends ConsumerWidget {
|
||||
final List<Task> tasks;
|
||||
const _TaskList({required this.tasks});
|
||||
final String search;
|
||||
const _TaskList({required this.tasks, this.search = ''});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (tasks.isEmpty) {
|
||||
return const Center(child: Text('No tasks here.'));
|
||||
return Center(
|
||||
child: Text(
|
||||
search.isEmpty ? 'No tasks here.' : 'No tasks match "$search".',
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: tasks.length,
|
||||
@@ -107,7 +153,8 @@ class _TaskList extends ConsumerWidget {
|
||||
leading: _priorityIcon(task.priority),
|
||||
title: Text(task.title),
|
||||
subtitle: task.dueDate != null
|
||||
? Text('Due: ${task.dueDate!.toLocal().toString().substring(0, 10)}')
|
||||
? Text(
|
||||
'Due: ${task.dueDate!.toLocal().toString().substring(0, 10)}')
|
||||
: null,
|
||||
onTap: () => context.push(
|
||||
Routes.taskEdit.replaceFirst(':id', '${task.id}'),
|
||||
|
||||
Reference in New Issue
Block a user