From 647b36837ed1ec8e63b0ee046ae3874992f74be0 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Sun, 1 Mar 2026 11:49:46 -0500 Subject: [PATCH] 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 --- lib/screens/notes/notes_list_screen.dart | 213 +++++++++++++---------- lib/screens/tasks/tasks_list_screen.dart | 177 +++++++++++-------- 2 files changed, 221 insertions(+), 169 deletions(-) diff --git a/lib/screens/notes/notes_list_screen.dart b/lib/screens/notes/notes_list_screen.dart index 2835939..70ad07f 100644 --- a/lib/screens/notes/notes_list_screen.dart +++ b/lib/screens/notes/notes_list_screen.dart @@ -21,104 +21,133 @@ class _NotesListScreenState extends ConsumerState { final notesAsync = ref.watch(notesProvider); return Scaffold( - 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: (_, _) => Center( - child: Column( - mainAxisSize: MainAxisSize.min, + body: Stack( + children: [ + Column( 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'), + if (_showSearch) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + Expanded( + 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}'), + ), + ); + }, + ), + ); + }, + ), ), ], ), - ), - 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".', + // Floating search button — only visible when search is closed + if (!_showSearch) + Positioned( + top: 4, + right: 4, + child: IconButton( + icon: const Icon(Icons.search), + tooltip: 'Search', + onPressed: () => setState(() => _showSearch = true), ), - ); - } - - 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( heroTag: 'notes_fab', diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 0d5594f..9173f9e 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -36,90 +36,113 @@ class _TasksListScreenState extends ConsumerState final tasksAsync = ref.watch(tasksProvider); return Scaffold( - appBar: AppBar( - 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: [ + body: Column( + children: [ + // Search field — appears above the tabs when active 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 [ - Tab(text: 'To Do'), - Tab(text: 'In Progress'), - Tab(text: 'Done'), - ], - ), - ), - 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'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + decoration: const InputDecoration( + hintText: 'Search tasks…', + 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 = ''; + }), + ), + ], ), + ), + // 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), + ), ], ), - ), - data: (tasks) { - final filtered = _search.isEmpty - ? tasks - : tasks - .where((t) => - t.title.toLowerCase().contains(_search) || - (t.description ?? '').toLowerCase().contains(_search)) - .toList(); + // Content + Expanded( + child: 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'), + ), + ], + ), + ), + data: (tasks) { + final filtered = _search.isEmpty + ? tasks + : tasks + .where((t) => + t.title.toLowerCase().contains(_search) || + (t.description ?? '') + .toLowerCase() + .contains(_search)) + .toList(); - final todo = - filtered.where((t) => t.status == TaskStatus.todo).toList(); - final inProgress = - filtered.where((t) => t.status == TaskStatus.inProgress).toList(); - final done = - filtered.where((t) => t.status == TaskStatus.done).toList(); + final todo = filtered + .where((t) => t.status == TaskStatus.todo) + .toList(); + final inProgress = filtered + .where((t) => t.status == TaskStatus.inProgress) + .toList(); + final done = filtered + .where((t) => t.status == TaskStatus.done) + .toList(); - return RefreshIndicator( - onRefresh: () => ref.refresh(tasksProvider.future), - child: TabBarView( - controller: _tabs, - children: [ - _TaskList(tasks: todo, search: _search), - _TaskList(tasks: inProgress, search: _search), - _TaskList(tasks: done, search: _search), - ], + return RefreshIndicator( + onRefresh: () => ref.refresh(tasksProvider.future), + child: TabBarView( + controller: _tabs, + children: [ + _TaskList(tasks: todo, search: _search), + _TaskList(tasks: inProgress, search: _search), + _TaskList(tasks: done, search: _search), + ], + ), + ); + }, ), - ); - }, + ), + ], ), floatingActionButton: FloatingActionButton( heroTag: 'tasks_fab',