miler map
This commit is contained in:
@@ -37,6 +37,19 @@ class ColorConstants {
|
||||
static const Color surfaceTint = Color(0xFF960019);
|
||||
|
||||
// Primary Red
|
||||
/// ── The app bar's own red ──
|
||||
///
|
||||
/// The brand bar is painted in a two-stop gradient, and these are its stops.
|
||||
/// They were literals inside `home_header.dart`, which was fine while the bar
|
||||
/// was the only thing wearing them — Home's trip panel wears the same red
|
||||
/// now, and a panel that is *nearly* the app bar's colour reads as a mistake
|
||||
/// rather than as a family. One definition, so they cannot drift apart.
|
||||
///
|
||||
/// Not [primary]: that is the accent the app draws buttons and rules in, and
|
||||
/// it is a cooler, deeper crimson. The bar has always been this warmer brick.
|
||||
static const Color brandBarTop = Color(0xFFA51C1C);
|
||||
static const Color brandBarBottom = Color(0xFF8B1A1A);
|
||||
|
||||
static const Color primary = Color(0xFF960019);
|
||||
static const Color onPrimary = Color(0xFFFFFFFF);
|
||||
static const Color primaryContainer = Color(0xFFC01227);
|
||||
|
||||
@@ -3,41 +3,26 @@ import 'package:flutter/material.dart';
|
||||
class FontConstants {
|
||||
/// ── The app's typeface ──
|
||||
///
|
||||
/// Poppins: geometric, OFL-licensed, and the one every screen now reads from.
|
||||
/// It replaced Manrope, which was itself a stand-in for Uber Move.
|
||||
/// Manrope: a free, OFL-licensed geometric sans, bundled as a single
|
||||
/// variable font (`assets/fonts/Manrope/Manrope-VF.ttf`) and chosen as the
|
||||
/// closest legal stand-in for Uber Move, which is proprietary.
|
||||
///
|
||||
/// **Read this before changing it.** The family was already declared in
|
||||
/// `pubspec.yaml` as `poppins`, with a lowercase path on every one of its
|
||||
/// seven assets, while the files on disk are `assets/fonts/Poppins/Poppins-*`.
|
||||
/// Asset bundling is case-sensitive on both platforms, so not one of them
|
||||
/// resolved: anything asking for the family fell back to Roboto and no error
|
||||
/// was raised anywhere. The declaration is fixed and all seven static weights
|
||||
/// are registered — Poppins ships as separate files rather than a variable
|
||||
/// font, and without explicit 800/900 entries Flutter synthesises them by
|
||||
/// smearing SemiBold, which is what makes a heading look muddy at exactly the
|
||||
/// size it is meant to be read from.
|
||||
/// ── The weight scale is calibrated for THIS family ──
|
||||
/// **Poppins was tried here and reverted.** It is declared in `pubspec.yaml`
|
||||
/// and its seven static weights do resolve, so it is one string away if it is
|
||||
/// ever wanted again — but it runs optically far heavier than Manrope at
|
||||
/// every grade: geometric, wide bowls, thick stems. Swapping the family
|
||||
/// alone turned the whole app bold, headings especially, and stepping the
|
||||
/// weight scale down a grade to compensate was not enough to undo it.
|
||||
///
|
||||
/// Poppins runs optically heavier than Manrope at every grade: it is
|
||||
/// geometric, its bowls are wide and its stems thick, so `w800` here is far
|
||||
/// darker on the page than `w800` was in a variable Manrope. The whole scale
|
||||
/// was tuned against Manrope and came across a grade too heavy — headings
|
||||
/// read as shouting and body copy as bold.
|
||||
///
|
||||
/// Every weight in `lib/` (outside `lib/xpress`, which is Proxima Nova) was
|
||||
/// stepped down one grade: 900→800, 800→700, 700→600, 600→500. The *relative*
|
||||
/// hierarchy is untouched — each level is still one step from its neighbour —
|
||||
/// so nothing lost its rank; the whole run just sits where it should for this
|
||||
/// typeface.
|
||||
///
|
||||
/// **If the family changes again, re-tune this.** A weight is a number, not a
|
||||
/// design: what it looks like belongs to the font.
|
||||
static const String fontFamily = 'Poppins';
|
||||
/// **The weight scale below is calibrated for THIS family.** If the family
|
||||
/// changes again, re-tune it — a weight is a number, not a design; what it
|
||||
/// looks like belongs to the font.
|
||||
static const String fontFamily = 'Manrope';
|
||||
|
||||
/// Only ever reached for a glyph Poppins does not carry. Manrope covers the
|
||||
/// Only ever reached for a glyph Manrope does not carry. Manrope covers the
|
||||
/// Latin and currency sets the app uses, so this is a safety net rather than
|
||||
/// a second typeface in the design.
|
||||
static String fontFamilyFallback = 'Manrope';
|
||||
static String fontFamilyFallback = 'PlusJakartaSans';
|
||||
|
||||
// Typography scale
|
||||
static const double displayLg = 32.0;
|
||||
@@ -50,21 +35,21 @@ class FontConstants {
|
||||
|
||||
static TextStyle displayLgStyle = const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.64,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static TextStyle headlineMdStyle = const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.24,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static TextStyle headlineSmStyle = const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
@@ -83,7 +68,7 @@ class FontConstants {
|
||||
|
||||
static TextStyle labelBoldStyle = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
|
||||
@@ -36,13 +36,10 @@ class AppTheme {
|
||||
},
|
||||
),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
// Manrope first: it is bundled, so its coverage is guaranteed on every
|
||||
// device, and it is what backs Poppins up for anything Poppins does not
|
||||
// carry. The platform families behind it are the last resort. Poppins is
|
||||
// missing U+2192 among others — a glyph the app no longer renders as a
|
||||
// character, but the net belongs here rather than in a rule nobody
|
||||
// remembers.
|
||||
fontFamilyFallback: const ['Manrope', 'Roboto', 'sans-serif'],
|
||||
// Manrope is the family above and is bundled, so its coverage is
|
||||
// guaranteed on every device; these are only reached for a glyph it does
|
||||
// not carry, and the platform families are the last resort.
|
||||
fontFamilyFallback: const ['Roboto', 'sans-serif'],
|
||||
textTheme: ThemeData.light().textTheme.apply(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
@@ -116,7 +113,7 @@ class AppTheme {
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
@@ -153,7 +150,7 @@ class AppTheme {
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
@@ -162,7 +159,7 @@ class AppTheme {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _primary,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -64,7 +64,7 @@ class MilerType {
|
||||
/// The one thing a screen is about. One per screen, or none.
|
||||
static TextStyle get hero => TextStyle(
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.9,
|
||||
height: 1.08,
|
||||
color: ColorConstants.slateText,
|
||||
@@ -74,7 +74,7 @@ class MilerType {
|
||||
/// App bar titles and sheet headings.
|
||||
static TextStyle get pageTitle => TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.6,
|
||||
height: 1.15,
|
||||
color: ColorConstants.slateText,
|
||||
@@ -84,7 +84,7 @@ class MilerType {
|
||||
/// A stop name in a list, a section heading inside a screen.
|
||||
static TextStyle get cardTitle => TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.4,
|
||||
height: 1.2,
|
||||
color: ColorConstants.slateText,
|
||||
@@ -94,7 +94,7 @@ class MilerType {
|
||||
/// Addresses, running copy, the title line of a compact row.
|
||||
static TextStyle get body => TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
height: 1.35,
|
||||
color: ColorConstants.slateText,
|
||||
@@ -105,7 +105,7 @@ class MilerType {
|
||||
/// explains, and explanation is secondary to the thing being explained.
|
||||
static TextStyle get caption => TextStyle(
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.4,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: _family,
|
||||
@@ -114,7 +114,7 @@ class MilerType {
|
||||
/// Metadata, timestamps, chip text, the second line of a row.
|
||||
static TextStyle get label => TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.35,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: _family,
|
||||
@@ -124,7 +124,7 @@ class MilerType {
|
||||
/// expected to be read outdoors.
|
||||
static TextStyle get micro => TextStyle(
|
||||
fontSize: 12.5.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: _family,
|
||||
@@ -134,7 +134,7 @@ class MilerType {
|
||||
/// the reason most groups in this app need no outline.
|
||||
static TextStyle get eyebrow => TextStyle(
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.2,
|
||||
height: 1.3,
|
||||
color: ColorConstants.secondaryText,
|
||||
@@ -150,7 +150,7 @@ class MilerType {
|
||||
/// number, resolved without `.sp`.
|
||||
static const TextStyle pageTitleFixed = TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.6,
|
||||
height: 1.15,
|
||||
color: ColorConstants.slateText,
|
||||
@@ -165,7 +165,7 @@ class MilerType {
|
||||
/// them lines up.
|
||||
static TextStyle figure(double size, {Color? color}) => TextStyle(
|
||||
fontSize: size.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
height: 1.1,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@@ -183,7 +183,7 @@ class MilerType {
|
||||
? 15
|
||||
: 14)
|
||||
.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.1,
|
||||
color: ColorConstants.onAccent,
|
||||
fontFamily: _family,
|
||||
@@ -198,7 +198,7 @@ class MilerType {
|
||||
extension MilerTypeTint on TextStyle {
|
||||
TextStyle on(Color color) => copyWith(color: color);
|
||||
|
||||
TextStyle get bold => copyWith(fontWeight: FontWeight.w700);
|
||||
TextStyle get bold => copyWith(fontWeight: FontWeight.w800);
|
||||
|
||||
TextStyle get semibold => copyWith(fontWeight: FontWeight.w600);
|
||||
TextStyle get semibold => copyWith(fontWeight: FontWeight.w700);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user