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.
99 lines
3.1 KiB
Dart
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.')),
|
|
);
|
|
}
|