Files
rippr/lib/src/ui/router.dart
uhryniuk a14f5b0134 V3-07: route drawing (pins and straight lines)
Route plans are their own entity (RoutePlan/Waypoint, schema v3), never joined into ride totals or the rides list. RoutePlanRepository recomputes straight-line distance after every waypoint mutation. RoutePlannerScreen supports tap-to-add, tap-to-delete, and hand-rolled drag-to-move via MapCamera's screen/latlng conversions.

Fixed a real bug found by testing: FlutterMap's initialCenter/initialZoom are read once at construction, so building the map before the waypoints stream's first emission froze the camera on null-island. Also documents a genuine ten-minute test hang traced to Stream.first on a Drift watch() query inside testWidgets, which needs a pump-driven Timer that flutter_test's fake zone never advances on its own.
2026-08-17 19:15:18 -05:00

99 lines
3.1 KiB
Dart

/// Ported from `com.rippr.ui.RipprNavHost`.
///
/// Three destinations, same shape as the Compose original. `go_router` replaces
/// `navigation-compose`; the path parameter is parsed explicitly, mirroring the Kotlin
/// comment about ids arriving as the wrong type when the argument type is left implicit.
library;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'detail/trip_detail_screen.dart';
import 'record/record_screen.dart';
import 'routes/route_planner_screen.dart';
import 'routes/routes_list_screen.dart';
import 'settings/settings_screen.dart';
import 'trips/trips_screen.dart';
abstract final class Routes {
static const record = '/';
static const trips = '/trips';
static const tripDetail = '/trip/:tripId';
static const settings = '/settings';
static const routes = '/routes';
static const routePlanner = '/routes/:routeId';
static String detailFor(int tripId) => '/trip/$tripId';
static String plannerFor(int routeId) => '/routes/$routeId';
}
GoRouter buildRouter() => GoRouter(
initialLocation: Routes.record,
routes: [
GoRoute(
path: Routes.record,
builder: (context, state) => RecordScreen(
onOpenTrips: () => context.push(Routes.trips),
onOpenSettings: () => context.push(Routes.settings),
onOpenRoutes: () => context.push(Routes.routes),
),
),
GoRoute(
path: Routes.routes,
builder: (context, state) => RoutesListScreen(
onOpenRoute: (id) => context.push(Routes.plannerFor(id)),
onBack: () => context.pop(),
),
),
GoRoute(
path: Routes.routePlanner,
builder: (context, state) {
final id = int.tryParse(state.pathParameters['routeId'] ?? '');
if (id == null) return const _NotFound();
return RoutePlannerScreen(routeId: id, onBack: () => context.pop());
},
),
GoRoute(
path: Routes.settings,
builder: (context, state) =>
SettingsScreen(onBack: () => context.pop()),
),
GoRoute(
path: Routes.trips,
builder: (context, state) => TripsScreen(
onOpenTrip: (id) => context.push(Routes.detailFor(id)),
onBack: () => context.pop(),
),
),
GoRoute(
path: Routes.tripDetail,
builder: (context, state) {
// Parsed explicitly. A malformed or missing id must land on a real screen
// saying so, never on a blank one or a crash.
final id = int.tryParse(state.pathParameters['tripId'] ?? '');
if (id == null) {
return const _NotFound();
}
return TripDetailScreen(tripId: id, onBack: () => context.pop());
},
),
],
errorBuilder: (context, state) => const _NotFound(),
);
class _NotFound extends StatelessWidget {
const _NotFound();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () =>
context.canPop() ? context.pop() : context.go(Routes.record),
),
),
body: const Center(child: Text('That ride could not be found.')),
);
}