# Fabled App Overhaul — Plan 2: Navigation (Shell + Library + Cleanup) > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Prerequisite:** Plan 1 (theme + capture queue) must be complete. **Goal:** Replace the 4-tab shell (Notes · Tasks · Projects · Chat) with a 3-tab shell (Briefing · Library · Chat), build the unified LibraryScreen with filter pills and inline search, and delete the five screens made redundant by this change. **Architecture:** `app.dart`'s `_Shell` is rewritten for 3 tabs. A new `LibraryScreen` replaces the three separate list screens with a unified `GET /api/notes` + `GET /api/tasks` feed filtered by pill selection. A `LibraryItemCard` widget renders note, task, and project rows uniformly. The Briefing tab shows a placeholder screen until Plan 3 ships. Dead screens are deleted. **Tech Stack:** Flutter/Dart, Riverpod, GoRouter, Dio (existing), `GradientButton` from Plan 1 theme. --- ## File Map **Create:** - `lib/screens/briefing/briefing_screen.dart` — placeholder (full implementation in Plan 3) - `lib/screens/library/library_screen.dart` — unified list with filter pills + search - `lib/widgets/library_item_card.dart` — note / task / project row widget **Modify:** - `lib/app.dart` — rewrite `_Shell` for 3 tabs; add `/briefing` and `/library` routes; remove dead routes - `lib/core/constants.dart` — add `briefing` and `library` route constants - `lib/providers/api_client_provider.dart` — no changes needed (existing notes/tasks providers reused) **Delete:** - `lib/screens/notes/notes_list_screen.dart` - `lib/screens/tasks/tasks_list_screen.dart` - `lib/screens/projects/project_list_screen.dart` - `lib/screens/chat/conversations_list_screen.dart` - `lib/screens/quick_capture/quick_capture_screen.dart` --- ## Chunk 1: Constants + Placeholder Briefing Screen ### Task 1: Add route constants **Files:** - Modify: `lib/core/constants.dart` - [ ] **Step 1: Read `lib/core/constants.dart`** Check the existing `Routes` class. It will have constants like `notes`, `tasks`, etc. - [ ] **Step 2: Add new constants** Add to the `Routes` class: ```dart static const briefing = '/briefing'; static const library = '/library'; ``` - [ ] **Step 3: Commit** ```bash git add lib/core/constants.dart git commit -m "feat: add briefing and library route constants" ``` --- ### Task 2: Briefing placeholder screen **Files:** - Create: `lib/screens/briefing/briefing_screen.dart` This is a placeholder — Plan 3 will replace it entirely. It just renders a centred message so the tab is navigable. - [ ] **Step 1: Create `lib/screens/briefing/briefing_screen.dart`** ```dart import 'package:flutter/material.dart'; /// Placeholder — full implementation in Plan 3. class BriefingScreen extends StatelessWidget { const BriefingScreen({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: Text('Briefing', style: theme.textTheme.titleLarge), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.wb_sunny_outlined, size: 48, color: theme.colorScheme.primary), const SizedBox(height: 16), Text('Briefing coming soon', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text('Your daily briefing will appear here.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant)), ], ), ), ); } } ``` - [ ] **Step 2: Analyze** ```bash flutter analyze lib/screens/briefing/briefing_screen.dart ``` Expected: no errors. - [ ] **Step 3: Commit** ```bash git add lib/screens/briefing/briefing_screen.dart git commit -m "feat: briefing placeholder screen" ``` --- ## Chunk 2: Library Screen ### Task 3: LibraryItemCard widget **Files:** - Create: `lib/widgets/library_item_card.dart` A unified card that renders a note row, a task row, or a project row depending on item type. Tapping navigates to the appropriate detail screen. - [ ] **Step 1: Read the existing data models** Check `lib/data/models/note.dart`, `lib/data/models/task.dart`, `lib/data/models/project.dart` to confirm field names before writing the widget. - [ ] **Step 2: Create `lib/widgets/library_item_card.dart`** ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../core/constants.dart'; import '../data/models/note.dart'; import '../data/models/project.dart'; import '../data/models/task.dart'; import '../providers/tasks_provider.dart'; // ── Note card ──────────────────────────────────────────────────────────────── class NoteLibraryCard extends StatelessWidget { final Note note; const NoteLibraryCard({super.key, required this.note}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: InkWell( onTap: () => context.push( Routes.noteDetail.replaceFirst(':id', '${note.id}')), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.article_outlined, size: 15, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 6), Expanded( child: Text( note.title.isNotEmpty ? note.title : 'Untitled', style: theme.textTheme.titleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( _relativeTime(note.updatedAt), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), ), ], ), if (note.body.isNotEmpty) ...[ const SizedBox(height: 4), Text( note.body.replaceAll('\n', ' '), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (note.tags.isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 4, runSpacing: 2, children: note.tags .take(4) .map((t) => Chip( label: Text(t), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, )) .toList(), ), ], ], ), ), ), ); } } // ── Task card ──────────────────────────────────────────────────────────────── class TaskLibraryCard extends ConsumerWidget { final Task task; const TaskLibraryCard({super.key, required this.task}); Color _priorityColor(BuildContext context, String? priority) { final cs = Theme.of(context).colorScheme; return switch (priority) { 'high' => const Color(0xFFEF4444), 'medium' => const Color(0xFFF59E0B), _ => cs.onSurfaceVariant, }; } IconData _statusIcon(String status) => switch (status) { 'done' => Icons.check_circle, 'in_progress' => Icons.timelapse, _ => Icons.radio_button_unchecked, }; Color _statusColor(BuildContext context, String status) { final cs = Theme.of(context).colorScheme; return switch (status) { 'done' => const Color(0xFF22C55E), 'in_progress' => cs.primary, _ => cs.onSurfaceVariant, }; } String _nextStatus(String current) => switch (current) { 'todo' => 'in_progress', 'in_progress' => 'done', _ => 'todo', }; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: InkWell( onTap: () => context.push( Routes.taskEdit.replaceFirst(':id', '${task.id}')), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.fromLTRB(8, 10, 14, 10), child: Row( children: [ // Status cycle button IconButton( icon: Icon( _statusIcon(task.status), color: _statusColor(context, task.status), ), onPressed: () => ref .read(tasksProvider.notifier) .updateStatus(task.id, _nextStatus(task.status)), tooltip: 'Cycle status', visualDensity: VisualDensity.compact, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title.isNotEmpty ? task.title : 'Untitled', style: theme.textTheme.titleSmall?.copyWith( decoration: task.status == 'done' ? TextDecoration.lineThrough : null, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (task.dueDate != null) ...[ const SizedBox(height: 2), Text( 'Due ${DateFormat.MMMd().format(task.dueDate!)}', style: theme.textTheme.labelSmall?.copyWith( color: task.dueDate!.isBefore(DateTime.now()) && task.status != 'done' ? const Color(0xFFEF4444) : theme.colorScheme.onSurfaceVariant, ), ), ], ], ), ), if (task.priority != null && task.priority != 'none') Container( width: 8, height: 8, decoration: BoxDecoration( color: _priorityColor(context, task.priority), shape: BoxShape.circle, ), ), ], ), ), ), ); } } // ── Project card ───────────────────────────────────────────────────────────── class ProjectLibraryCard extends StatelessWidget { final Project project; const ProjectLibraryCard({super.key, required this.project}); Color _parseColor(String? hex) { if (hex == null || hex.isEmpty) return const Color(0xFF6366F1); try { return Color(int.parse(hex.replaceFirst('#', '0xFF'))); } catch (_) { return const Color(0xFF6366F1); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final color = _parseColor(project.color); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), clipBehavior: Clip.antiAlias, child: InkWell( onTap: () {}, // Projects have no detail screen in this app borderRadius: BorderRadius.circular(14), child: Row( children: [ // Colour strip Container(width: 6, height: 64, color: color), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( project.title, style: theme.textTheme.titleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), if (project.description?.isNotEmpty == true) ...[ const SizedBox(height: 2), Text( project.description!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), ), const SizedBox(width: 12), ], ), ), ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── String _relativeTime(DateTime? dt) { if (dt == null) return ''; final diff = DateTime.now().difference(dt); if (diff.inMinutes < 1) return 'just now'; if (diff.inHours < 1) return '${diff.inMinutes}m ago'; if (diff.inDays < 1) return '${diff.inHours}h ago'; if (diff.inDays < 7) return '${diff.inDays}d ago'; return DateFormat.MMMd().format(dt); } ``` Note: `Routes.noteDetail` and `Routes.taskEdit` use path patterns like `/notes/:id` — check `lib/core/constants.dart` for exact values and adjust `replaceFirst` calls accordingly. Also note: `tasksProvider.notifier.updateStatus()` — check `lib/providers/tasks_provider.dart` to verify this method exists. If not, add it (it calls `PATCH /api/tasks/:id` with `{status}`). Also: `intl` package may need adding to `pubspec.yaml` — check if it's already a transitive dependency via `flutter pub deps`. If not, add `intl: ^0.19.0`. - [ ] **Step 3: Analyze** ```bash flutter analyze lib/widgets/library_item_card.dart ``` Fix any type errors (common: nullable field access, missing `fromJson` fields on models). - [ ] **Step 4: Commit** ```bash git add lib/widgets/library_item_card.dart git commit -m "feat: LibraryItemCard — note, task, and project row widgets" ``` --- ### Task 4: LibraryScreen **Files:** - Create: `lib/screens/library/library_screen.dart` - [ ] **Step 1: Create `lib/screens/library/library_screen.dart`** ```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/note.dart'; import '../../data/models/task.dart'; import '../../providers/notes_provider.dart'; import '../../providers/projects_provider.dart'; import '../../providers/tasks_provider.dart'; import '../../widgets/library_item_card.dart'; enum _LibraryFilter { all, notes, tasks, projects } enum _TaskStatusFilter { all, todo, inProgress, done } class LibraryScreen extends ConsumerStatefulWidget { const LibraryScreen({super.key}); @override ConsumerState createState() => _LibraryScreenState(); } class _LibraryScreenState extends ConsumerState { _LibraryFilter _filter = _LibraryFilter.all; _TaskStatusFilter _taskStatus = _TaskStatusFilter.all; bool _searchActive = false; String _searchQuery = ''; final _searchController = TextEditingController(); @override void dispose() { _searchController.dispose(); super.dispose(); } bool _matchesSearch(String text) { if (_searchQuery.isEmpty) return true; return text.toLowerCase().contains(_searchQuery.toLowerCase()); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final notesAsync = ref.watch(notesProvider); final tasksAsync = ref.watch(tasksProvider); final projectsAsync = ref.watch(projectsProvider); return Scaffold( appBar: AppBar( title: _searchActive ? TextField( controller: _searchController, autofocus: true, decoration: const InputDecoration( hintText: 'Search…', border: InputBorder.none, isDense: true, ), onChanged: (q) => setState(() => _searchQuery = q), ) : Text('Library', style: theme.textTheme.titleLarge), actions: [ IconButton( icon: Icon(_searchActive ? Icons.close : Icons.search), onPressed: () => setState(() { _searchActive = !_searchActive; if (!_searchActive) { _searchQuery = ''; _searchController.clear(); } }), ), ], ), body: Column( children: [ // ── Filter pills ────────────────────────────────────────────────── SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), child: Row( children: _LibraryFilter.values.map((f) { final label = switch (f) { _LibraryFilter.all => 'All', _LibraryFilter.notes => 'Notes', _LibraryFilter.tasks => 'Tasks', _LibraryFilter.projects => 'Projects', }; final selected = _filter == f; return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(label), selected: selected, onSelected: (_) => setState(() { _filter = f; _taskStatus = _TaskStatusFilter.all; }), selectedColor: theme.colorScheme.primary.withValues(alpha: 0.18), checkmarkColor: theme.colorScheme.primary, ), ); }).toList(), ), ), // ── Task status sub-filter (Tasks pill only) ─────────────────── if (_filter == _LibraryFilter.tasks) SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), child: Row( children: _TaskStatusFilter.values.map((s) { final label = switch (s) { _TaskStatusFilter.all => 'All', _TaskStatusFilter.todo => 'To Do', _TaskStatusFilter.inProgress => 'In Progress', _TaskStatusFilter.done => 'Done', }; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(label), selected: _taskStatus == s, onSelected: (_) => setState(() => _taskStatus = s), selectedColor: theme.colorScheme.secondary.withValues(alpha: 0.15), ), ); }).toList(), ), ), const Divider(height: 1), // ── Content ─────────────────────────────────────────────────────── Expanded( child: switch (_filter) { _LibraryFilter.notes => _buildNotesList(notesAsync), _LibraryFilter.tasks => _buildTasksList(tasksAsync), _LibraryFilter.projects => _buildProjectsList(projectsAsync), _LibraryFilter.all => _buildAllList(notesAsync, tasksAsync), }, ), ], ), floatingActionButton: FloatingActionButton( onPressed: () => _showCreateSheet(context), child: const Icon(Icons.add), ), ); } Widget _buildNotesList(AsyncValue> notesAsync) { return notesAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), data: (notes) { final filtered = notes .where((n) => _matchesSearch(n.title) || _matchesSearch(n.body)) .toList(); if (filtered.isEmpty) { return const Center(child: Text('No notes found')); } return RefreshIndicator( onRefresh: () async => ref.invalidate(notesProvider), child: ListView.builder( itemCount: filtered.length, itemBuilder: (_, i) => NoteLibraryCard(note: filtered[i]), ), ); }, ); } Widget _buildTasksList(AsyncValue> tasksAsync) { return tasksAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), data: (tasks) { var filtered = tasks .where((t) => _matchesSearch(t.title)) .toList(); if (_taskStatus != _TaskStatusFilter.all) { final status = switch (_taskStatus) { _TaskStatusFilter.todo => 'todo', _TaskStatusFilter.inProgress => 'in_progress', _TaskStatusFilter.done => 'done', _ => '', }; filtered = filtered.where((t) => t.status == status).toList(); } if (filtered.isEmpty) { return const Center(child: Text('No tasks found')); } return RefreshIndicator( onRefresh: () async => ref.invalidate(tasksProvider), child: ListView.builder( itemCount: filtered.length, itemBuilder: (_, i) => TaskLibraryCard(task: filtered[i]), ), ); }, ); } Widget _buildProjectsList(AsyncValue projects) { return projects.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), data: (data) { final list = data as List; if (list.isEmpty) { return const Center(child: Text('No projects')); } return RefreshIndicator( onRefresh: () async => ref.invalidate(projectsProvider), child: ListView.builder( itemCount: list.length, itemBuilder: (_, i) => ProjectLibraryCard(project: list[i]), ), ); }, ); } Widget _buildAllList( AsyncValue> notesAsync, AsyncValue> tasksAsync, ) { final notes = notesAsync.valueOrNull ?? []; final tasks = tasksAsync.valueOrNull ?? []; // Merge and sort by updatedAt desc final items = <(DateTime, Widget)>[]; for (final n in notes) { if (_matchesSearch(n.title) || _matchesSearch(n.body)) { items.add((n.updatedAt ?? DateTime(0), NoteLibraryCard(note: n))); } } for (final t in tasks) { if (_matchesSearch(t.title)) { items.add((t.updatedAt ?? DateTime(0), TaskLibraryCard(task: t))); } } items.sort((a, b) => b.$1.compareTo(a.$1)); if (notesAsync.isLoading || tasksAsync.isLoading) { return const Center(child: CircularProgressIndicator()); } if (items.isEmpty) { return const Center(child: Text('Nothing here yet')); } return RefreshIndicator( onRefresh: () async { ref.invalidate(notesProvider); ref.invalidate(tasksProvider); }, child: ListView.builder( itemCount: items.length, itemBuilder: (_, i) => items[i].$2, ), ); } void _showCreateSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (_) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.article_outlined), title: const Text('New note'), onTap: () { Navigator.pop(context); context.push(Routes.noteNew); }, ), ListTile( leading: const Icon(Icons.check_box_outlined), title: const Text('New task'), onTap: () { Navigator.pop(context); context.push(Routes.taskNew); }, ), ], ), ), ); } } ``` - [ ] **Step 2: Check model field names** Read `lib/data/models/note.dart` and `lib/data/models/task.dart`. Verify: - `Note` has: `id`, `title`, `body`, `tags` (List), `updatedAt` (DateTime?) - `Task` has: `id`, `title`, `status`, `priority`, `dueDate` (DateTime?), `updatedAt` (DateTime?) - `Project` has: `id`, `title`, `description`, `color` Adjust field names in `LibraryScreen` and `LibraryItemCard` if they differ. - [ ] **Step 3: Verify `tasksProvider.notifier` has `updateStatus()`** Check `lib/providers/tasks_provider.dart`. If `updateStatus(int id, String status)` doesn't exist, add it: ```dart Future updateStatus(int id, String status) async { await ref.read(tasksRepositoryProvider).updateStatus(id, status); ref.invalidateSelf(); } ``` And add to `lib/data/repositories/tasks_repository.dart`: ```dart Future updateStatus(int id, String status) async { await _api.updateStatus(id, status); } ``` And add to `lib/data/api/tasks_api.dart`: ```dart Future updateStatus(int id, String status) async { try { await _dio.patch('/api/tasks/$id/status', data: {'status': status}); } on DioException catch (e) { throw dioToApp(e); } } ``` - [ ] **Step 4: Analyze** ```bash flutter analyze lib/screens/library/ lib/widgets/library_item_card.dart ``` Fix any errors. - [ ] **Step 5: Commit** ```bash git add lib/screens/library/ lib/widgets/library_item_card.dart git commit -m "feat: LibraryScreen — unified notes/tasks/projects with filter pills and search" ``` --- ## Chunk 3: Shell Rewrite + Dead Code Cleanup ### Task 5: Rewrite the shell and routes **Files:** - Modify: `lib/app.dart` — `_Shell` class, `routerProvider` routes list - [ ] **Step 1: Update imports in `lib/app.dart`** Add: ```dart import 'screens/briefing/briefing_screen.dart'; import 'screens/library/library_screen.dart'; ``` Remove imports for deleted screens (they will be deleted next): ```dart // Remove these: import 'screens/notes/notes_list_screen.dart'; import 'screens/tasks/tasks_list_screen.dart'; import 'screens/projects/project_list_screen.dart'; import 'screens/chat/conversations_list_screen.dart'; ``` - [ ] **Step 2: Update the shell tab list** Find `static const _tabs = [Routes.notes, Routes.tasks, Routes.projects, Routes.conversations];` Replace with: ```dart static const _tabs = [Routes.briefing, Routes.library, Routes.conversations]; ``` - [ ] **Step 3: Update NavigationBar destinations** Replace the 4-destination `NavigationBar` with: ```dart bottomNavigationBar: NavigationBar( selectedIndex: index, onDestinationSelected: (i) => context.go(_tabs[i]), destinations: const [ NavigationDestination( icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: 'Briefing', ), NavigationDestination( icon: Icon(Icons.library_books_outlined), selectedIcon: Icon(Icons.library_books), label: 'Library', ), NavigationDestination( icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: 'Chat', ), ], ), ``` - [ ] **Step 4: Update NavigationRail destinations (wide layout)** Replace the 4-destination `NavigationRail` with: ```dart NavigationRail( selectedIndex: index, onDestinationSelected: (i) => context.go(_tabs[i]), labelType: NavigationRailLabelType.all, destinations: const [ NavigationRailDestination( icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: Text('Briefing'), ), NavigationRailDestination( icon: Icon(Icons.library_books_outlined), selectedIcon: Icon(Icons.library_books), label: Text('Library'), ), NavigationRailDestination( icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: Text('Chat'), ), ], ), ``` - [ ] **Step 5: Update the ShellRoute in routerProvider** Find the `ShellRoute` with its child `GoRoute`s. Replace: ```dart ShellRoute( builder: (context, state, child) => _Shell(child: child), routes: [ GoRoute(path: Routes.briefing, builder: (_, _) => const BriefingScreen()), GoRoute(path: Routes.library, builder: (_, _) => const LibraryScreen()), GoRoute(path: Routes.conversations, builder: (_, _) => const ConversationsInlineScreen()), ], ), ``` For the Chat tab: the existing `ConversationsListScreen` is being deleted. Replace it with an inline conversations list built directly in the ShellRoute, or create a minimal `lib/screens/chat/conversations_tab_screen.dart` that replaces it. The simplest approach: move the conversations list content inline. Create `lib/screens/chat/conversations_tab_screen.dart`: ```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 '../../providers/chat_provider.dart'; class ConversationsTabScreen extends ConsumerWidget { const ConversationsTabScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final convsAsync = ref.watch(conversationsProvider); return Scaffold( appBar: AppBar( title: Text('Chat', style: theme.textTheme.titleLarge), actions: [ IconButton( icon: const Icon(Icons.add), tooltip: 'New conversation', onPressed: () async { final conv = await ref .read(conversationsProvider.notifier) .create('New conversation'); if (context.mounted) { context.push(Routes.chat.replaceFirst(':id', '${conv.id}')); } }, ), ], ), body: convsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), data: (convs) { if (convs.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.chat_bubble_outline, size: 48, color: theme.colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text('No conversations yet', style: theme.textTheme.titleMedium), const SizedBox(height: 8), FilledButton.icon( icon: const Icon(Icons.add), label: const Text('Start a conversation'), onPressed: () async { final conv = await ref .read(conversationsProvider.notifier) .create('New conversation'); if (context.mounted) { context.push( Routes.chat.replaceFirst(':id', '${conv.id}')); } }, ), ], ), ); } return RefreshIndicator( onRefresh: () async => ref.invalidate(conversationsProvider), child: ListView.builder( itemCount: convs.length, itemBuilder: (ctx, i) { final c = convs[i]; return ListTile( leading: const Icon(Icons.chat_bubble_outline), title: Text(c.title, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: c.updatedAt != null ? Text( _relativeTime(c.updatedAt!), style: theme.textTheme.labelSmall, ) : null, trailing: IconButton( icon: const Icon(Icons.delete_outline), onPressed: () => _confirmDelete(context, ref, c.id, c.title), ), onTap: () => ctx.push( Routes.chat.replaceFirst(':id', '${c.id}')), ); }, ), ); }, ), ); } Future _confirmDelete( BuildContext context, WidgetRef ref, int id, String title) async { final ok = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Delete conversation?'), content: Text('"$title" will be permanently deleted.'), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')), ], ), ); if (ok == true) { await ref.read(conversationsProvider.notifier).delete(id); } } } String _relativeTime(DateTime dt) { final diff = DateTime.now().difference(dt); if (diff.inMinutes < 1) return 'just now'; if (diff.inHours < 1) return '${diff.inMinutes}m ago'; if (diff.inDays < 1) return '${diff.inHours}h ago'; if (diff.inDays < 7) return '${diff.inDays}d ago'; return '${dt.day}/${dt.month}/${dt.year}'; } ``` Then in the ShellRoute, use `ConversationsTabScreen`: ```dart import 'screens/chat/conversations_tab_screen.dart'; // ... GoRoute(path: Routes.conversations, builder: (_, _) => const ConversationsTabScreen()), ``` - [ ] **Step 6: Update initial location** In `GoRouter(initialLocation: ...)`, change: ```dart initialLocation: Routes.splash, ``` (This stays — the splash screen handles auth redirect. After auth, redirect to briefing. Update the redirect logic to send authenticated users to `Routes.briefing` instead of falling through to `Routes.notes`.) In the `redirect` callback, ensure unauthenticated users still go to login, and authenticated users landing on `/` or `/notes`/`/tasks`/`/projects` are redirected to `/briefing`. The simplest: no change needed — GoRouter will use the shell's first tab (`Routes.briefing`) naturally as the initial shell route. - [ ] **Step 7: Analyze** ```bash flutter analyze lib/app.dart lib/screens/ ``` Fix any errors. - [ ] **Step 8: Commit** ```bash git add lib/app.dart lib/screens/chat/conversations_tab_screen.dart lib/core/constants.dart git commit -m "feat: 3-tab shell — Briefing, Library, Chat" ``` --- ### Task 6: Delete dead screens **Files:** - Delete: 5 screen files - [ ] **Step 1: Delete the dead files** ```bash cd /home/bvandeusen/Nextcloud/Projects/fabled_app git rm lib/screens/notes/notes_list_screen.dart git rm lib/screens/tasks/tasks_list_screen.dart git rm lib/screens/projects/project_list_screen.dart git rm lib/screens/chat/conversations_list_screen.dart git rm lib/screens/quick_capture/quick_capture_screen.dart ``` - [ ] **Step 2: Verify no remaining imports** ```bash grep -r "notes_list_screen\|tasks_list_screen\|project_list_screen\|conversations_list_screen\|quick_capture_screen" lib/ ``` Expected: no output. - [ ] **Step 3: Analyze full project** ```bash flutter analyze ``` Expected: zero errors. - [ ] **Step 4: Run and verify** ```bash flutter run --debug ``` Verify: - App opens to Briefing tab (placeholder screen) - Library tab shows unified list with filter pills - Tasks filter → status sub-filter appears - Tapping a note navigates to `NoteDetailScreen` - Chat tab shows conversations list - FAB on Library → bottom sheet with "New note" / "New task" - [ ] **Step 5: Commit** ```bash git commit -m "chore: delete dead list screens replaced by LibraryScreen" ``` --- ## Verification Checklist - [ ] `flutter analyze` — zero errors - [ ] App opens to Briefing tab - [ ] Library → All: notes and tasks interleaved, sorted by date - [ ] Library → Notes: only notes - [ ] Library → Tasks: tasks + status sub-filter works - [ ] Library → Projects: project cards with colour strip - [ ] Library search: typing filters results live - [ ] Task status cycle: tapping checkbox icon on a task card cycles status - [ ] FAB → bottom sheet → "New note" / "New task" navigates correctly - [ ] Chat tab: conversations list, new conversation creates and navigates to ChatScreen - [ ] No references to deleted files remain