Remove AppBar gap from notes and tasks list screens

Notes: no AppBar at all; a floating search icon (top-right corner)
triggers an inline search field above the list rather than an AppBar.

Tasks: no AppBar; TabBar sits directly at the top of the body, flush
with the quick-capture bar. The search icon lives to the right of the
tabs and expands a search field above them when tapped.

Both changes eliminate the ~56dp toolbar gap that was visible between
the quick-capture bar and the list content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:49:46 -05:00
parent 2a35fe5532
commit 647b36837e
2 changed files with 221 additions and 169 deletions
+121 -92
View File
@@ -21,104 +21,133 @@ class _NotesListScreenState extends ConsumerState<NotesListScreen> {
final notesAsync = ref.watch(notesProvider); final notesAsync = ref.watch(notesProvider);
return Scaffold( return Scaffold(
appBar: AppBar( body: Stack(
automaticallyImplyLeading: false, children: [
title: _showSearch Column(
? 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: (_, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.cloud_off, size: 48), if (_showSearch)
const SizedBox(height: 12), Padding(
const Text('Could not load notes.'), padding: const EdgeInsets.symmetric(horizontal: 4),
const SizedBox(height: 4), child: Row(
TextButton( children: [
onPressed: () => ref.invalidate(notesProvider), Expanded(
child: const Text('Retry'), child: TextField(
autofocus: true,
decoration: const InputDecoration(
hintText: 'Search notes…',
border: InputBorder.none,
prefixIcon: Icon(Icons.search),
),
onChanged: (v) =>
setState(() => _search = v.trim().toLowerCase()),
),
),
IconButton(
icon: const Icon(Icons.close),
tooltip: 'Close search',
onPressed: () => setState(() {
_showSearch = false;
_search = '';
}),
),
],
),
),
Expanded(
child: notesAsync.when(
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (_, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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'),
),
],
),
),
data: (notes) {
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: filtered.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, 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(
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}'),
),
);
},
),
);
},
),
), ),
], ],
), ),
), // Floating search button — only visible when search is closed
data: (notes) { if (!_showSearch)
final filtered = _search.isEmpty Positioned(
? notes top: 4,
: notes right: 4,
.where((n) => child: IconButton(
n.title.toLowerCase().contains(_search) || icon: const Icon(Icons.search),
n.body.toLowerCase().contains(_search)) tooltip: 'Search',
.toList(); onPressed: () => setState(() => _showSearch = true),
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: filtered.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, 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(
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}'),
),
);
},
), ),
); ],
},
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'notes_fab', heroTag: 'notes_fab',
+100 -77
View File
@@ -36,90 +36,113 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
final tasksAsync = ref.watch(tasksProvider); final tasksAsync = ref.watch(tasksProvider);
return Scaffold( return Scaffold(
appBar: AppBar( body: Column(
automaticallyImplyLeading: false, children: [
title: _showSearch // Search field — appears above the tabs when active
? TextField(
autofocus: true,
decoration: const InputDecoration(
hintText: 'Search tasks…',
border: InputBorder.none,
),
onChanged: (v) =>
setState(() => _search = v.trim().toLowerCase()),
)
: null,
actions: [
if (_showSearch) if (_showSearch)
IconButton( Padding(
icon: const Icon(Icons.close), padding: const EdgeInsets.symmetric(horizontal: 4),
tooltip: 'Close search', child: Row(
onPressed: () => setState(() { children: [
_showSearch = false; Expanded(
_search = ''; child: TextField(
}), autofocus: true,
) decoration: const InputDecoration(
else hintText: 'Search tasks…',
IconButton( border: InputBorder.none,
icon: const Icon(Icons.search), prefixIcon: Icon(Icons.search),
tooltip: 'Search', ),
onPressed: () => setState(() => _showSearch = true), onChanged: (v) =>
), setState(() => _search = v.trim().toLowerCase()),
], ),
bottom: TabBar( ),
controller: _tabs, IconButton(
tabs: const [ icon: const Icon(Icons.close),
Tab(text: 'To Do'), tooltip: 'Close search',
Tab(text: 'In Progress'), onPressed: () => setState(() {
Tab(text: 'Done'), _showSearch = false;
], _search = '';
), }),
), ),
body: tasksAsync.when( ],
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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'),
), ),
),
// Tab bar row — search icon sits to the right of the tabs
Row(
children: [
Expanded(
child: TabBar(
controller: _tabs,
tabs: const [
Tab(text: 'To Do'),
Tab(text: 'In Progress'),
Tab(text: 'Done'),
],
),
),
if (!_showSearch)
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Search',
onPressed: () => setState(() => _showSearch = true),
),
], ],
), ),
), // Content
data: (tasks) { Expanded(
final filtered = _search.isEmpty child: tasksAsync.when(
? tasks loading: () => const Center(child: CircularProgressIndicator()),
: tasks error: (_, _) => Center(
.where((t) => child: Column(
t.title.toLowerCase().contains(_search) || mainAxisSize: MainAxisSize.min,
(t.description ?? '').toLowerCase().contains(_search)) children: [
.toList(); 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'),
),
],
),
),
data: (tasks) {
final filtered = _search.isEmpty
? tasks
: tasks
.where((t) =>
t.title.toLowerCase().contains(_search) ||
(t.description ?? '')
.toLowerCase()
.contains(_search))
.toList();
final todo = final todo = filtered
filtered.where((t) => t.status == TaskStatus.todo).toList(); .where((t) => t.status == TaskStatus.todo)
final inProgress = .toList();
filtered.where((t) => t.status == TaskStatus.inProgress).toList(); final inProgress = filtered
final done = .where((t) => t.status == TaskStatus.inProgress)
filtered.where((t) => t.status == TaskStatus.done).toList(); .toList();
final done = filtered
.where((t) => t.status == TaskStatus.done)
.toList();
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => ref.refresh(tasksProvider.future), onRefresh: () => ref.refresh(tasksProvider.future),
child: TabBarView( child: TabBarView(
controller: _tabs, controller: _tabs,
children: [ children: [
_TaskList(tasks: todo, search: _search), _TaskList(tasks: todo, search: _search),
_TaskList(tasks: inProgress, search: _search), _TaskList(tasks: inProgress, search: _search),
_TaskList(tasks: done, search: _search), _TaskList(tasks: done, search: _search),
], ],
),
);
},
), ),
); ),
}, ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'tasks_fab', heroTag: 'tasks_fab',