miler map

This commit is contained in:
2026-09-09 12:55:23 +05:30
parent 074cc0eccf
commit 127fa062ed
143 changed files with 4315 additions and 2291 deletions

View File

@@ -9,6 +9,7 @@ import 'package:get/get.dart';
import 'package:shimmer/shimmer.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/views/helpers/constants/narrative.dart';
import 'package:miler/views/helpers/constants/miler_type.dart';
import 'package:miler/views/helpers/widgets/miler_app_bar.dart'
show milerBrandSurface;
@@ -64,10 +65,6 @@ class MilerBand extends StatelessWidget {
/// translucent wash is the wrong tool for a surface.
final Color? color;
/// An accent stripe down the leading edge, for a band whose state has to
/// read without being examined. Null draws no stripe and no inset.
final Color? rail;
/// Overrides the standard inset. Pass only where the child owns its own
/// horizontal padding — a row that has to bleed to the band's edge.
final EdgeInsets? padding;
@@ -76,21 +73,36 @@ class MilerBand extends StatelessWidget {
super.key,
required this.child,
this.color,
this.rail,
this.padding,
});
/// The inset a band keeps around its content.
///
/// One number for the whole app. With the bands running edge to edge, their
/// padding is the only thing left aligning the page's left margin — two
/// bands a couple of points apart is exactly how a column of text stops
/// looking like a column.
static double get pad => 20.w;
/// One number for the whole app: two bands a couple of points apart is
/// exactly how a column of text stops looking like a column.
static double get pad => 18.w;
/// The gap of page ground between one band and the next. This is the only
/// separator a band has, so it is a real gap rather than a hairline.
static double get gap => 10.h;
/// The page margin the band sits in.
///
/// ── Bands used to run edge to edge, and the page paid for it ──
///
/// A full-bleed band has no shape of its own, so the only thing separating
/// one from the next was a [gap] of bare page showing through — which turned
/// the ground into a set of stripes between the content. On the old slate
/// canvas those stripes were dark grey bars, and the screen read as a stack
/// of unrelated slabs rather than as a form.
///
/// A band is a card now: inset from both edges, rounded, on a hairline and a
/// soft lift. The gap between two of them is air rather than a bar, and the
/// page's left margin is the card's edge instead of the padding inside it.
static double get gutter => 14.w;
/// The air between one band and the next.
static double get gap => 12.h;
/// The corner. Sections of a form, so a little tighter than a page-level
/// panel — see [Narrative.radiusCard], which is the shape Home's cards take.
static double get radius => Narrative.radiusInner;
@override
Widget build(BuildContext context) {
@@ -102,14 +114,16 @@ class MilerBand extends StatelessWidget {
duration: DesignConstants.motionState,
curve: Curves.easeOut,
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: gutter),
padding: padding ?? EdgeInsets.symmetric(horizontal: pad, vertical: 16.h),
decoration: BoxDecoration(
color: color ?? ColorConstants.pureSurface,
border: rail == null
? null
: Border(
left: BorderSide(color: rail!, width: 3.w),
),
borderRadius: BorderRadius.circular(radius),
// The hairline and the lift together, the same recipe every other card
// in the app is built on — so a form section and a panel on Home read
// as the same kind of object. See [Narrative.cardBox].
border: Border.all(color: Narrative.cardBorder, width: 1),
boxShadow: Narrative.cardShadow,
),
child: child,
);
@@ -261,7 +275,7 @@ class PrimaryButton extends StatelessWidget {
maxLines: 1,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
),
),
@@ -324,7 +338,7 @@ class SecondaryButton extends StatelessWidget {
style: TextStyle(
color: c,
fontSize: 14,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
),
),
@@ -370,7 +384,7 @@ class StatusChip extends StatelessWidget {
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
@@ -440,7 +454,7 @@ class SectionHeader extends StatelessWidget {
maxLines: 1,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
color: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
@@ -456,7 +470,7 @@ class SectionHeader extends StatelessWidget {
action!,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: ColorConstants.primary,
fontFamily: FontConstants.fontFamily,
),
@@ -540,7 +554,7 @@ class InfoBanner extends StatelessWidget {
color: color,
fontSize: 13,
height: 1.35,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
@@ -722,7 +736,7 @@ class AppLoader extends StatelessWidget {
label!,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
@@ -1629,7 +1643,7 @@ class MilerButton extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.1,
color: fg,
fontFamily: FontConstants.fontFamily,
@@ -2042,7 +2056,7 @@ class StopSourceLine extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.1,
color: accent,
fontFamily: FontConstants.fontFamily,
@@ -2063,7 +2077,7 @@ class StopSourceLine extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
// Tabular, because these are read as codes and a column of
// them down a list should line up.
fontFeatures: const [FontFeature.tabularFigures()],
@@ -2124,7 +2138,7 @@ class OrderStatusTag extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.1,
color: color,
fontFamily: FontConstants.fontFamily,

View File

@@ -143,7 +143,7 @@ class _Message extends StatelessWidget {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
@@ -156,7 +156,7 @@ class _Message extends StatelessWidget {
style: TextStyle(
fontSize: 14.sp,
height: 1.45,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
@@ -172,7 +172,7 @@ class _Message extends StatelessWidget {
'Try again',
style: TextStyle(
fontSize: 14.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
fontFamily: FontConstants.fontFamily,
),
),

View File

@@ -319,7 +319,7 @@ class MilerAvatar extends StatelessWidget {
// Fixed .sp off a fixed box: the circle does not grow with
// the system font scale, so neither can the letter inside it.
fontSize: 19.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
height: 1,
color: onBrand
? ColorConstants.primary

View File

@@ -149,7 +149,7 @@ class _MilerAttribution extends StatelessWidget {
'© OpenStreetMap',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
@@ -345,7 +345,7 @@ class MilerNumberPin extends StatelessWidget {
style: const TextStyle(
fontSize: 14,
height: 1,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
@@ -512,14 +512,102 @@ class _MilerLegMapState extends State<MilerLegMap> {
}
}
/// How many times to ask for the road before settling for the bearing.
///
/// ── Why one attempt was never enough ──
///
/// `route()` answers an empty list for every failure — a 429, a cold DNS
/// lookup that outran the timeout, a tunnel — and this asked exactly once,
/// from `initState`. The only other trigger is `didUpdateWidget`, and it
/// fires on a change of *endpoint*: a rider standing at a door changes
/// neither. So a single unlucky moment at the instant the sheet opened left
/// `_road` empty for as long as the sheet stayed open, and the leg drew as a
/// dashed straight line across a route the router would have returned a
/// second later. That is the "it shows a straight line, not the real route"
/// report, and it is a retry bug rather than a routing one — the public OSRM
/// server answers this leg in under a second when asked again.
///
/// Three tries with a widening gap. Cheap, bounded, and it costs nothing at
/// all in the normal case where the first answer is the road.
static const int _routeAttempts = 3;
static const Duration _retryBackoff = Duration(milliseconds: 700);
/// Distinguishes a stale in-flight request from the current one, so a leg
/// that changed while the network was slow cannot have the previous leg's
/// road painted onto it when the old request finally lands.
int _fetchGeneration = 0;
/// ── The backoff has to be cancellable, not just ignored ──
///
/// A bare `Future.delayed` checks `mounted` on the far side of the wait, which
/// is too late for the one observer that counts: the timer is still pending
/// when the tree goes, and Flutter's test binding asserts on exactly that —
/// *"A Timer is still pending even after the widget tree was disposed"*. It
/// failed 15 tests that open this sheet and dispose it inside the retry gap.
///
/// Off a test bench it is the same leak in slower motion: a rider flicking
/// through six stops leaves six timers running against six dead widgets.
///
/// So the wait is a [Timer] this state owns and [dispose] cancels — and
/// disposing also completes the pending waiter, so the retry loop wakes,
/// sees `!mounted`, and unwinds instead of parking forever on a future
/// nothing will ever finish.
Timer? _retryTimer;
Completer<void>? _retryWait;
@override
void dispose() {
_retryTimer?.cancel();
_retryTimer = null;
final waiter = _retryWait;
_retryWait = null;
if (waiter != null && !waiter.isCompleted) waiter.complete();
super.dispose();
}
/// A cancellable [Future.delayed]. See [_retryTimer].
Future<void> _wait(Duration d) {
final completer = Completer<void>();
_retryWait = completer;
_retryTimer = Timer(d, () {
_retryTimer = null;
if (identical(_retryWait, completer)) _retryWait = null;
if (!completer.isCompleted) completer.complete();
});
return completer.future;
}
Future<void> _fetchRoad() async {
final from = widget.from;
if (from == null) return;
final road = await MilerRouter.route(from, widget.to);
if (!mounted || road.isEmpty) return;
setState(() => _road = road);
// The road can bulge outside the straight-line bounds; refit around it.
_fit();
final to = widget.to;
final generation = ++_fetchGeneration;
for (var attempt = 1; attempt <= _routeAttempts; attempt++) {
final road = await MilerRouter.route(from, to);
// Superseded, or gone from the tree, while the request was in flight.
if (!mounted || generation != _fetchGeneration) return;
if (road.isNotEmpty) {
setState(() => _road = road);
// The road can bulge outside the straight-line bounds; refit around it.
_fit();
return;
}
if (attempt < _routeAttempts) {
await _wait(_retryBackoff * attempt);
if (!mounted || generation != _fetchGeneration) return;
}
}
// Says which leg gave up, so a rider reporting a dashed line can be matched
// against the `[MAP] OSRM …` line that explains why.
debugPrint(
'[MAP] no road after $_routeAttempts attempts for '
'${from.latitude},${from.longitude} -> ${to.latitude},${to.longitude} — '
'drawing the dashed bearing instead.',
);
}
void _fit() {

View File

@@ -209,7 +209,7 @@ class MilerSheetHeader extends StatelessWidget {
title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.45,
height: 1.2,
color: ColorConstants.slateText,
@@ -222,7 +222,7 @@ class MilerSheetHeader extends StatelessWidget {
subtitle!,
style: TextStyle(
fontSize: 13.5.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
height: 1.35,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -337,8 +337,8 @@ class MilerSheetChoiceRow extends StatelessWidget {
color: selected ? accent : ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
fontWeight: selected
? FontWeight.w700
: FontWeight.w500,
? FontWeight.w800
: FontWeight.w600,
),
),
),

View File

@@ -295,7 +295,7 @@ class _MilerSlideActionState extends State<MilerSlideAction>
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w900,
letterSpacing: -0.3,
color: ColorConstants.onAccent,
fontFamily: FontConstants.fontFamily,