Book a pickup, track it to delivery — the rebuilt customer app. - Design language from doormile-screens.html: brand #8F0F06, Manrope + Geist Mono (variable fonts), bordered cards instead of shadows, crimson brand headers, sliding tab indicator, mono for anything read digit by digit. - lib/data (one live API implementation, plus a debug-only offline fake), lib/state, lib/ui (tokens, widgets, screens). - 84 tests, plus a design snapshot harness that renders every screen with the real fonts: flutter test test/design_snapshot_test.dart --run-skipped --update-goldens This replaces the previous app (pubspec 'doormile', app id com.doormile.customer). That tree remains in history at 6c7d656; note its android/app/google-services.json is not carried over, and the application id here is in.doormile.customer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
Dart
77 lines
2.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
import '../../data/map_config.dart';
|
|
import '../tokens.dart';
|
|
|
|
/// Turns the configured tile provider ([DmMapConfig]) into a flutter_map layer.
|
|
///
|
|
/// The one bridge between map configuration and map rendering: no widget in
|
|
/// this app knows a tile URL, a subdomain list or an API key, and changing
|
|
/// provider never reaches a screen.
|
|
class DmMapTiles {
|
|
DmMapTiles._();
|
|
|
|
/// What shows through before a tile paints, or when one never arrives.
|
|
static const ground = Color(0xFFF0EDE9);
|
|
|
|
static DmMapConfig get config => DmMapConfig.instance;
|
|
|
|
static TileLayer layer(
|
|
BuildContext context, {
|
|
DmMapStyle style = DmMapStyle.streets,
|
|
}) {
|
|
final map = config;
|
|
return TileLayer(
|
|
urlTemplate: map.urlFor(style),
|
|
subdomains: map.subdomains,
|
|
// Identifies the app to the provider so our traffic is attributable and
|
|
// can be rate-limited or allow-listed properly.
|
|
userAgentPackageName: DmMapConfig.packageName,
|
|
tileProvider: NetworkTileProvider(
|
|
headers: {'User-Agent': map.userAgent},
|
|
// A failed tile is a cosmetic event — the ground colour covers it.
|
|
silenceExceptions: true,
|
|
),
|
|
retinaMode: map.retina && RetinaMode.isHighDensity(context),
|
|
maxNativeZoom: map.maxNativeZoom,
|
|
// Defaults, stated so they are not "tuned" upward later: the app fetches
|
|
// only the tiles it is about to draw plus the standard render margin.
|
|
// Bulk prefetching or scraping a provider's tiles is out of bounds.
|
|
keepBuffer: 2,
|
|
panBuffer: 1,
|
|
tileDisplay: const TileDisplay.fadeIn(
|
|
duration: Duration(milliseconds: 160),
|
|
),
|
|
// Offline, or under `flutter test`, tiles simply never arrive.
|
|
errorTileCallback: (_, _, _) {},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The credit line the tile licence requires. Small, quiet, and on every map —
|
|
/// its text comes from the provider, so it stays correct after a swap.
|
|
class DmMapAttribution extends StatelessWidget {
|
|
const DmMapAttribution({super.key, this.dark = false});
|
|
|
|
/// Slightly stronger contrast for maps with busy cartography behind it.
|
|
final bool dark;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IgnorePointer(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: DmColors.surface.withValues(alpha: dark ? 0.92 : 0.82),
|
|
borderRadius: DmRadius.all(DmRadius.pill),
|
|
),
|
|
child: Text(
|
|
DmMapTiles.config.attribution,
|
|
style: DmText.small.copyWith(fontSize: 9.5, color: DmColors.ink3),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|