522 lines
16 KiB
Markdown
522 lines
16 KiB
Markdown
# Android Nav Restructure & Projects Staleness Fix Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Replace the Projects bottom-nav tab with a "More" bottom sheet giving access to Projects, News, and Calendar; fix stale task data when returning from task edit.
|
||
|
||
**Architecture:** The shell's `_tabs` list shrinks from 4 to 3 entries; the 4th nav destination ("More") is intercepted in `onDestinationSelected` to show a `showModalBottomSheet` instead of navigating. Projects moves from a ShellRoute to a top-level push route. News and Calendar are added as stub push-routes. `_tabIndex` maps the three overflow paths to index 3 so "More" highlights correctly. The staleness fix moves the task-tap callback out of the stateless `_TaskRow` widget into the parent `ConsumerState` where `ref` is available, using `.then()` to invalidate after pop.
|
||
|
||
**Tech Stack:** Flutter, GoRouter, Riverpod, Material 3
|
||
|
||
---
|
||
|
||
## Files
|
||
|
||
| File | Action | What changes |
|
||
|------|--------|-------------|
|
||
| `lib/core/constants.dart` | Modify | Add `news` and `calendar` route constants |
|
||
| `lib/app.dart` | Modify | Remove Projects from ShellRoute, add 3 push routes, shrink `_tabs`, add `_showMoreSheet`, update `_tabIndex`, update both nav widgets |
|
||
| `lib/screens/library/project_tasks_screen.dart` | Modify | Add `onTap: VoidCallback` to `_TaskRow`; add `_openTask` method to state; pass callback at both call sites |
|
||
|
||
---
|
||
|
||
### Task 1: Add route constants
|
||
|
||
**Files:**
|
||
- Modify: `lib/core/constants.dart`
|
||
|
||
- [ ] **Step 1: Add `news` and `calendar` to `Routes`**
|
||
|
||
Open `lib/core/constants.dart`. The file currently ends with:
|
||
```dart
|
||
abstract class Routes {
|
||
static const splash = '/';
|
||
static const setup = '/setup';
|
||
static const login = '/login';
|
||
static const notes = '/notes';
|
||
static const noteDetail = '/notes/:id';
|
||
static const noteEdit = '/notes/:id/edit';
|
||
static const noteNew = '/notes/new';
|
||
static const tasks = '/tasks';
|
||
static const taskNew = '/tasks/new';
|
||
static const taskEdit = '/tasks/:id/edit';
|
||
static const knowledge = '/knowledge';
|
||
static const projects = '/projects';
|
||
static const projectEdit = '/projects/:id/edit';
|
||
static const conversations = '/chat';
|
||
static const chat = '/chat/:id';
|
||
static const quickCapture = '/quick-capture';
|
||
static const settings = '/settings';
|
||
static const briefing = '/briefing';
|
||
static const projectTasks = '/projects/:id/tasks';
|
||
}
|
||
```
|
||
|
||
Add two constants after `briefing`:
|
||
```dart
|
||
abstract class Routes {
|
||
static const splash = '/';
|
||
static const setup = '/setup';
|
||
static const login = '/login';
|
||
static const notes = '/notes';
|
||
static const noteDetail = '/notes/:id';
|
||
static const noteEdit = '/notes/:id/edit';
|
||
static const noteNew = '/notes/new';
|
||
static const tasks = '/tasks';
|
||
static const taskNew = '/tasks/new';
|
||
static const taskEdit = '/tasks/:id/edit';
|
||
static const knowledge = '/knowledge';
|
||
static const projects = '/projects';
|
||
static const projectEdit = '/projects/:id/edit';
|
||
static const conversations = '/chat';
|
||
static const chat = '/chat/:id';
|
||
static const quickCapture = '/quick-capture';
|
||
static const settings = '/settings';
|
||
static const briefing = '/briefing';
|
||
static const news = '/news';
|
||
static const calendar = '/calendar';
|
||
static const projectTasks = '/projects/:id/tasks';
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify no analysis errors**
|
||
|
||
```bash
|
||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||
flutter analyze lib/core/constants.dart
|
||
```
|
||
Expected: `No issues found!`
|
||
|
||
---
|
||
|
||
### Task 2: Restructure shell and add routes in `app.dart`
|
||
|
||
**Files:**
|
||
- Modify: `lib/app.dart`
|
||
|
||
This task has several sub-steps. Make them all before running analyze.
|
||
|
||
- [ ] **Step 1: Move Projects out of ShellRoute, add three push routes**
|
||
|
||
Find the `ShellRoute` block (currently lines ~142–162):
|
||
```dart
|
||
ShellRoute(
|
||
builder: (context, state, child) => _Shell(child: child),
|
||
routes: [
|
||
GoRoute(
|
||
path: Routes.briefing,
|
||
builder: (_, _) => const BriefingScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.knowledge,
|
||
builder: (_, _) => const KnowledgeScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.conversations,
|
||
builder: (_, _) => const ConversationsTabScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.projects,
|
||
builder: (_, _) => const ProjectsScreen(),
|
||
),
|
||
],
|
||
),
|
||
```
|
||
|
||
Replace with (Projects removed from shell; three new top-level routes added after the closing `],` of the ShellRoute):
|
||
```dart
|
||
ShellRoute(
|
||
builder: (context, state, child) => _Shell(child: child),
|
||
routes: [
|
||
GoRoute(
|
||
path: Routes.briefing,
|
||
builder: (_, _) => const BriefingScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.knowledge,
|
||
builder: (_, _) => const KnowledgeScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.conversations,
|
||
builder: (_, _) => const ConversationsTabScreen(),
|
||
),
|
||
],
|
||
),
|
||
GoRoute(
|
||
path: Routes.projects,
|
||
builder: (_, _) => const ProjectsScreen(),
|
||
),
|
||
GoRoute(
|
||
path: Routes.news,
|
||
builder: (_, _) => Scaffold(
|
||
appBar: AppBar(title: const Text('News')),
|
||
body: const Center(child: Text('News — coming soon')),
|
||
),
|
||
),
|
||
GoRoute(
|
||
path: Routes.calendar,
|
||
builder: (_, _) => Scaffold(
|
||
appBar: AppBar(title: const Text('Calendar')),
|
||
body: const Center(child: Text('Calendar — coming soon')),
|
||
),
|
||
),
|
||
```
|
||
|
||
- [ ] **Step 2: Shrink `_tabs` from 4 to 3 entries**
|
||
|
||
Find in `_ShellState`:
|
||
```dart
|
||
static const _tabs = [
|
||
Routes.briefing,
|
||
Routes.knowledge,
|
||
Routes.conversations,
|
||
Routes.projects,
|
||
];
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
static const _tabs = [
|
||
Routes.briefing,
|
||
Routes.knowledge,
|
||
Routes.conversations,
|
||
];
|
||
```
|
||
|
||
- [ ] **Step 3: Update `_tabIndex` to map overflow routes to index 3**
|
||
|
||
Find:
|
||
```dart
|
||
int _tabIndex(String location) {
|
||
for (var i = 0; i < _tabs.length; i++) {
|
||
if (location.startsWith(_tabs[i])) return i;
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
int _tabIndex(String location) {
|
||
for (var i = 0; i < _tabs.length; i++) {
|
||
if (location.startsWith(_tabs[i])) return i;
|
||
}
|
||
if (location.startsWith(Routes.projects) ||
|
||
location.startsWith(Routes.news) ||
|
||
location.startsWith(Routes.calendar)) return 3;
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Add `_showMoreSheet` method to `_ShellState`**
|
||
|
||
Add this method anywhere in `_ShellState`, e.g. just before `build`:
|
||
```dart
|
||
void _showMoreSheet(BuildContext context) {
|
||
showModalBottomSheet<void>(
|
||
context: context,
|
||
builder: (_) => SafeArea(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
ListTile(
|
||
leading: const Icon(Icons.folder_outlined),
|
||
title: const Text('Projects'),
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
context.push(Routes.projects);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.newspaper_outlined),
|
||
title: const Text('News'),
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
context.push(Routes.news);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.calendar_month_outlined),
|
||
title: const Text('Calendar'),
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
context.push(Routes.calendar);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Update `NavigationRail` — intercept index 3 and swap destination**
|
||
|
||
Find in the wide-layout branch:
|
||
```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.menu_book_outlined),
|
||
selectedIcon: Icon(Icons.menu_book),
|
||
label: Text('Knowledge'),
|
||
),
|
||
NavigationRailDestination(
|
||
icon: Icon(Icons.chat_bubble_outline),
|
||
selectedIcon: Icon(Icons.chat_bubble),
|
||
label: Text('Chat'),
|
||
),
|
||
NavigationRailDestination(
|
||
icon: Icon(Icons.folder_outlined),
|
||
selectedIcon: Icon(Icons.folder),
|
||
label: Text('Projects'),
|
||
),
|
||
],
|
||
),
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
NavigationRail(
|
||
selectedIndex: index,
|
||
onDestinationSelected: (i) {
|
||
if (i == 3) {
|
||
_showMoreSheet(context);
|
||
} else {
|
||
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.menu_book_outlined),
|
||
selectedIcon: Icon(Icons.menu_book),
|
||
label: Text('Knowledge'),
|
||
),
|
||
NavigationRailDestination(
|
||
icon: Icon(Icons.chat_bubble_outline),
|
||
selectedIcon: Icon(Icons.chat_bubble),
|
||
label: Text('Chat'),
|
||
),
|
||
NavigationRailDestination(
|
||
icon: Icon(Icons.more_horiz_outlined),
|
||
selectedIcon: Icon(Icons.more_horiz),
|
||
label: Text('More'),
|
||
),
|
||
],
|
||
),
|
||
```
|
||
|
||
- [ ] **Step 6: Update `NavigationBar` — intercept index 3 and swap destination**
|
||
|
||
Find in the narrow-layout branch:
|
||
```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.menu_book_outlined),
|
||
selectedIcon: Icon(Icons.menu_book),
|
||
label: 'Knowledge',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.chat_bubble_outline),
|
||
selectedIcon: Icon(Icons.chat_bubble),
|
||
label: 'Chat',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.folder_outlined),
|
||
selectedIcon: Icon(Icons.folder),
|
||
label: 'Projects',
|
||
),
|
||
],
|
||
),
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
bottomNavigationBar: NavigationBar(
|
||
selectedIndex: index,
|
||
onDestinationSelected: (i) {
|
||
if (i == 3) {
|
||
_showMoreSheet(context);
|
||
} else {
|
||
context.go(_tabs[i]);
|
||
}
|
||
},
|
||
destinations: const [
|
||
NavigationDestination(
|
||
icon: Icon(Icons.wb_sunny_outlined),
|
||
selectedIcon: Icon(Icons.wb_sunny),
|
||
label: 'Briefing',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.menu_book_outlined),
|
||
selectedIcon: Icon(Icons.menu_book),
|
||
label: 'Knowledge',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.chat_bubble_outline),
|
||
selectedIcon: Icon(Icons.chat_bubble),
|
||
label: 'Chat',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.more_horiz_outlined),
|
||
selectedIcon: Icon(Icons.more_horiz),
|
||
label: 'More',
|
||
),
|
||
],
|
||
),
|
||
```
|
||
|
||
- [ ] **Step 7: Verify no analysis errors**
|
||
|
||
```bash
|
||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||
flutter analyze lib/app.dart lib/core/constants.dart
|
||
```
|
||
Expected: `No issues found!`
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```bash
|
||
git add lib/core/constants.dart lib/app.dart
|
||
git commit -m "feat: replace Projects tab with More bottom sheet (news/calendar stubs)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Fix stale tasks after returning from task edit
|
||
|
||
**Files:**
|
||
- Modify: `lib/screens/library/project_tasks_screen.dart`
|
||
|
||
- [ ] **Step 1: Add `_openTask` method to `_ProjectTasksScreenState`**
|
||
|
||
`_ProjectTasksScreenState` is a `ConsumerState` — it has `ref` and `context`. Find the class body (look for `_cycleStatus` method as a landmark) and add `_openTask` as a sibling method:
|
||
|
||
```dart
|
||
void _openTask(int taskId) {
|
||
context
|
||
.push(Routes.taskEdit.replaceFirst(':id', '$taskId'))
|
||
.then((_) => ref.invalidate(projectTasksProvider(widget.projectId)));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Add `onTap` parameter to `_TaskRow`**
|
||
|
||
Find the `_TaskRow` class definition:
|
||
```dart
|
||
class _TaskRow extends StatelessWidget {
|
||
final Task task;
|
||
final TaskStatus effectiveStatus;
|
||
final VoidCallback onStatusTap;
|
||
|
||
const _TaskRow({
|
||
required this.task,
|
||
required this.effectiveStatus,
|
||
required this.onStatusTap,
|
||
});
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
class _TaskRow extends StatelessWidget {
|
||
final Task task;
|
||
final TaskStatus effectiveStatus;
|
||
final VoidCallback onStatusTap;
|
||
final VoidCallback onTap;
|
||
|
||
const _TaskRow({
|
||
required this.task,
|
||
required this.effectiveStatus,
|
||
required this.onStatusTap,
|
||
required this.onTap,
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: Use the `onTap` callback in `InkWell`**
|
||
|
||
Find inside `_TaskRow.build`:
|
||
```dart
|
||
child: InkWell(
|
||
onTap: () => context
|
||
.push(Routes.taskEdit.replaceFirst(':id', '${task.id}')),
|
||
```
|
||
|
||
Replace with:
|
||
```dart
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
```
|
||
|
||
- [ ] **Step 4: Pass `onTap` at both `_TaskRow` call sites**
|
||
|
||
There are two places in `_buildBody` where `_TaskRow` is instantiated. Update both:
|
||
|
||
**First call site (milestone tasks, around line 170):**
|
||
```dart
|
||
return _TaskRow(
|
||
task: task,
|
||
effectiveStatus: _effectiveStatus(task),
|
||
onStatusTap: () => _cycleStatus(task),
|
||
onTap: () => _openTask(task.id),
|
||
);
|
||
```
|
||
|
||
**Second call site (unassigned tasks, around line 197):**
|
||
```dart
|
||
return _TaskRow(
|
||
task: task,
|
||
effectiveStatus: _effectiveStatus(task),
|
||
onStatusTap: () => _cycleStatus(task),
|
||
onTap: () => _openTask(task.id),
|
||
);
|
||
```
|
||
|
||
- [ ] **Step 5: Verify no analysis errors**
|
||
|
||
```bash
|
||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||
flutter analyze lib/screens/library/project_tasks_screen.dart
|
||
```
|
||
Expected: `No issues found!`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add lib/screens/library/project_tasks_screen.dart
|
||
git commit -m "fix: invalidate project tasks on return from task edit screen"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Final check
|
||
|
||
- [ ] **Step 1: Full analyze**
|
||
|
||
```bash
|
||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||
flutter analyze
|
||
```
|
||
Expected: `No issues found!`
|