Scaffold the Flutter app with Italian l10n and region configuration
Creates the app skeleton and the seam everything else in the project hangs off: region-specific data lives in an asset file, not in code, so adding a region later is a new JSON file rather than a refactor. Notable choices: - applicationId and namespace are it.nuvolari.app rather than the doubled it.nuvolari.nuvolari that `flutter create` produces, with the Kotlin package and the iOS bundle identifiers moved to match. - SDK levels are pinned instead of inherited from `flutter.*`. Google Play requires API 36, and that is a release blocker rather than something to let a Flutter upgrade change silently. minSdk 24 is the highest floor the planned dependencies impose. - Riverpod and Dio-free for now, no code generation: see docs/stack-decisions.md. - Italian is the l10n source language, so app_it.arb is the template rather than a translation of an English original. The region parser rejects rather than repairs. An inverted bounding box, a map centre outside its own bounds, an unknown adapter name, a duplicate zone code or an empty attribution list all throw with the offending field named. Each of those would otherwise fail silently and visibly wrong: a swapped latitude and longitude renders the radar in the wrong place, an unknown adapter falling back to mock would show demo frames where live data was expected, and a missing attribution is a licence violation rather than a cosmetic gap. Tests run against the asset that actually ships and against a captured copy of the live ARPA CAP feed, so the eleven zone codes in the config are checked against the eleven the feed really emits rather than against a list retyped from documentation. Verified: dart format clean, flutter analyze 0 issues, 50 tests passing, flutter build appbundle --debug produces an AAB with applicationId it.nuvolari.app, minSdk 24, targetSdk 36. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/// Build-time configuration, supplied with `--dart-define-from-file=env.json`.
|
||||
///
|
||||
/// Every value has a working default, so the app builds and runs with no
|
||||
/// `env.json` at all: it starts in demo mode against bundled mock frames, with
|
||||
/// the offline map style and Google's test ad units. That keeps the repository
|
||||
/// free of secrets and keeps a fresh clone runnable.
|
||||
///
|
||||
/// Values are `const` reads of `String.fromEnvironment` so they are inlined and
|
||||
/// tree-shaken; they cannot be read from a file at run time.
|
||||
library;
|
||||
|
||||
class Env {
|
||||
const Env._();
|
||||
|
||||
/// Which region config to load from `assets/regions/<id>.json`.
|
||||
static const String regionId = String.fromEnvironment(
|
||||
'REGION_ID',
|
||||
defaultValue: 'piemonte',
|
||||
);
|
||||
|
||||
/// MapLibre style URL for the base map.
|
||||
///
|
||||
/// Empty means no key is configured, and [hasMapStyle] is false: the app
|
||||
/// falls back to a minimal style bundled in assets so development and tests
|
||||
/// work offline.
|
||||
static const String mapStyleUrl = String.fromEnvironment('MAP_STYLE_URL');
|
||||
|
||||
static bool get hasMapStyle => mapStyleUrl.isNotEmpty;
|
||||
|
||||
/// Base URL of the radar `manifest.json` published by our worker.
|
||||
///
|
||||
/// Empty forces the mock radar source regardless of [radarSource].
|
||||
static const String radarManifestUrl = String.fromEnvironment(
|
||||
'RADAR_MANIFEST_URL',
|
||||
);
|
||||
|
||||
/// Radar adapter override: `mock`, `dpc` or `arpa`.
|
||||
///
|
||||
/// Empty means "use whatever the region config says".
|
||||
static const String radarSource = String.fromEnvironment('RADAR_SOURCE');
|
||||
|
||||
/// Contact address embedded in the MET Norway User-Agent.
|
||||
///
|
||||
/// MET Norway blocks generic User-Agent strings, so forecasts are disabled
|
||||
/// rather than attempted when this is empty — see [hasForecastContact].
|
||||
static const String metnoUserAgentContact = String.fromEnvironment(
|
||||
'METNO_USER_AGENT_CONTACT',
|
||||
);
|
||||
|
||||
static bool get hasForecastContact => metnoUserAgentContact.isNotEmpty;
|
||||
|
||||
/// AdMob identifiers. Empty means Google's test units are used.
|
||||
static const String admobAppId = String.fromEnvironment('ADMOB_APP_ID');
|
||||
static const String admobBannerUnitId = String.fromEnvironment(
|
||||
'ADMOB_BANNER_UNIT_ID',
|
||||
);
|
||||
|
||||
static bool get hasAdMobConfig =>
|
||||
admobAppId.isNotEmpty && admobBannerUnitId.isNotEmpty;
|
||||
|
||||
/// True when nothing is configured and the app is running entirely on
|
||||
/// bundled data. Surfaced in the UI so demo frames are never mistaken for a
|
||||
/// real forecast.
|
||||
static bool get isDemoMode => radarManifestUrl.isEmpty;
|
||||
}
|
||||
Reference in New Issue
Block a user