UI-01: persistent 4-tab shell with a shared background map

Replaces the push/pop stack rooted at Record with a StatefulShellRoute
(Map/Rides/Plan/Settings), each tab keeping its own navigator so Trip Detail
and the route planner push within their own branch. AppShell/ShellScaffold
host one shared RideMap instance behind every tab -- full opacity and
interactive on Map, dimmed and non-interactive elsewhere -- so the camera
position and live path survive a tab switch instead of being refetched.
Every tab's AppBar/header is removed per the redesign's no-chrome mandate.

Fixes a bug this surfaced: RideMap rendered a structurally different tree
for empty vs. non-empty points, which crashed once the map became a
long-lived shell background instead of a fresh per-screen widget. Unified
the background-map tree shape so it no longer remounts mid-recording.

Verified on Medium_Phone_API_35: all four tabs, live recording surviving
tab switches with the camera preserved, and the real HOME-key lifecycle
tile-drop from V3-04 still firing correctly in the shell context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
2026-08-23 19:28:25 -05:00
parent 3e7878b6e1
commit 5ab4934a4d
10 changed files with 511 additions and 317 deletions

View File

@@ -21,10 +21,9 @@ final _selectionProvider = StateProvider.autoDispose<Set<int>>(
);
class TripsScreen extends ConsumerWidget {
const TripsScreen({super.key, this.onOpenTrip, this.onBack});
const TripsScreen({super.key, this.onOpenTrip});
final void Function(int tripId)? onOpenTrip;
final VoidCallback? onBack;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -44,18 +43,22 @@ class TripsScreen extends ConsumerWidget {
void clearSelection() =>
ref.read(_selectionProvider.notifier).state = <int>{};
// UI-01: no back/title header -- Rides is a tab now. The selection toolbar (Cancel/
// count/Merge/Delete) is functional, not chrome, so it stays -- it only appears
// while `selecting` is true.
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
if (selecting) ...[
if (selecting)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
TextButton(
onPressed: clearSelection,
child: const Text('Cancel'),
@@ -67,27 +70,7 @@ class TripsScreen extends ConsumerWidget {
style: TextStyle(fontSize: 14, color: colors.outline),
),
),
] else ...[
TextButton(
key: const Key('back'),
onPressed: onBack,
child: const Text('‹ Record'),
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'RIDES',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 3,
color: colors.primary,
),
),
),
],
const Spacer(),
if (selecting) ...[
const Spacer(),
TextButton(
key: const Key('merge'),
onPressed: canMerge
@@ -104,9 +87,8 @@ class TripsScreen extends ConsumerWidget {
),
),
],
],
),
),
),
Expanded(
child: switch (tripsAsync) {
AsyncValue(hasValue: true, value: final list)