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:
2026-09-10 11:24:26 +02:00
co-authored by Claude Opus 5
parent 3fcbb9f1c4
commit 85b2c99949
78 changed files with 4110 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/config/env.dart';
import 'core/region/region_repository.dart';
import 'l10n/app_localizations.dart';
void main() {
runApp(const ProviderScope(child: NuvolariApp()));
}
class NuvolariApp extends StatelessWidget {
const NuvolariApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FB2)),
),
home: const HomeScreen(),
);
}
}
/// Placeholder shell for milestone 1.
///
/// It exists to prove the pieces are wired: localisation resolves, the region
/// config parses out of the asset bundle, and demo mode is visible. The radar
/// map replaces it in milestone 2.
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final region = ref.watch(regionConfigProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
if (Env.isDemoMode)
MaterialBanner(
content: Text(l10n.demoModeBanner),
actions: const [SizedBox.shrink()],
),
Expanded(
child: Center(
child: switch (region) {
AsyncData(:final value) => Text(
value.displayName,
style: Theme.of(context).textTheme.headlineMedium,
),
AsyncError(:final error) => Padding(
padding: const EdgeInsets.all(24),
child: Text('${l10n.dataUnavailable}\n$error'),
),
_ => Text(l10n.loading),
},
),
),
],
),
);
}
}