This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/lib/screens/tasks/tasks_list_screen.dart
T
bvandeusen 647b36837e 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>
2026-03-01 11:49:46 -05:00

200 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/constants.dart';
import '../../data/models/task.dart';
import '../../providers/tasks_provider.dart';
class TasksListScreen extends ConsumerStatefulWidget {
const TasksListScreen({super.key});
@override
ConsumerState<TasksListScreen> createState() => _TasksListScreenState();
}
class _TasksListScreenState extends ConsumerState<TasksListScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabs;
bool _showSearch = false;
String _search = '';
@override
void initState() {
super.initState();
_tabs = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabs.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final tasksAsync = ref.watch(tasksProvider);
return Scaffold(
body: Column(
children: [
// Search field — appears above the tabs when active
if (_showSearch)
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),
),
],
),
// 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();
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',
onPressed: () => context.push(Routes.taskNew),
child: const Icon(Icons.add),
),
);
}
}
class _TaskList extends ConsumerWidget {
final List<Task> tasks;
final String search;
const _TaskList({required this.tasks, this.search = ''});
@override
Widget build(BuildContext context, WidgetRef ref) {
if (tasks.isEmpty) {
return Center(
child: Text(
search.isEmpty ? 'No tasks here.' : 'No tasks match "$search".',
),
);
}
return ListView.separated(
itemCount: tasks.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, i) {
final task = tasks[i];
return ListTile(
leading: _priorityIcon(task.priority),
title: Text(task.title),
subtitle: task.dueDate != null
? Text(
'Due: ${task.dueDate!.toLocal().toString().substring(0, 10)}')
: null,
onTap: () => context.push(
Routes.taskEdit.replaceFirst(':id', '${task.id}'),
),
);
},
);
}
Widget _priorityIcon(TaskPriority p) {
final color = switch (p) {
TaskPriority.high => Colors.red,
TaskPriority.medium => Colors.orange,
TaskPriority.low => Colors.green,
TaskPriority.none => Colors.grey,
};
return Icon(Icons.flag, color: color);
}
}