Compare commits

8 Commits
Author SHA1 Message Date
maxiwere45andClaude Opus 4.8 8608fc023c docs: sync ARCHITECTURE and README with actual codebase
Add missing providers (locale, theme, genFilter, caughtCount), all 6
pages, full widget inventory, core/l10n layers, and game mechanics
(hints, skips, shiny, bonuses, bilingual input, gen filter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:58:37 +02:00
adessen 8f93a18a70 patch guess langue issue 2026-06-23 14:39:16 +02:00
adessen 3d2f5be5e5 oups forget something 2026-06-23 14:26:59 +02:00
adessen 3984e94bc5 patch small issue with trad and theme 2026-06-23 14:17:27 +02:00
maxiwere45 47536843be hotfix: update README 2026-06-23 12:22:37 +02:00
maxiwere45 0952b372b4 hotfix: update default language 2026-06-23 12:16:38 +02:00
maxiwere45 9a41bd50a6 Merge: FR/EN internationalization (i18n) with language selector 2026-06-23 12:13:51 +02:00
maxiwere45andClaude Opus 4.8 a2a7ffd79f feat(i18n): add FR/EN internationalization with in-app language selector
- gen-l10n setup (flutter_localizations, intl, l10n.yaml, ARB en/fr)
- localeProvider (persisted) wired into MaterialApp
- language selector in the System page (alongside the color palette)
- all user-facing strings across screens moved to AppLocalizations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:13:51 +02:00
51 changed files with 2343 additions and 236 deletions
-5
View File
@@ -80,8 +80,3 @@ repository.
- shiny : `.../sprites/<id>/shiny.png`
- [Cris des Pokémon](https://pokemoncries.com/cries/1.mp3)
## Pistes d'amélioration
- Lecteur audio pour écouter le cri du Pokémon
- Internationalisation (FR / EN)
- Vue « mon équipe » (6 Pokémon choisis)
File diff suppressed because one or more lines are too long
+70 -8
View File
@@ -8,23 +8,74 @@ par Riverpod (providers manuels).
## Couches
### core (transversal)
- **`AppConstants`** : toutes les constantes métier en un seul endroit (vies, points, shiny odds,
plages de générations, clés SharedPreferences, URL API).
- **`logger`** : wrapper minimal autour de `dart:developer`.
### domain (Dart pur)
- **Entités** : `Pokemon`, immuable, sans dépendance Flutter/DB/API.
- **Repository (interface)** : `PokemonRepository` définit le contrat d'accès aux données.
- **Jeu** : `GameState` (état immuable) et `GameEngine` (règles pures, testables).
- `GameEngine` gère : soumission de réponse, vies, hints, skips, score, Shiny, bonus périodiques.
- `GameStatus` : `loading | playing | roundWon | gameOver`.
- `GuessResult` : `correct | wrong | gameOver | invalid`.
### data
- **DTO** : `PokemonDto` centralise tout le parsing JSON (API Tyradex + SQLite).
- **Datasources** : `PokemonLocalDataSource` (SQLite via sqflite), `PokemonRemoteDataSource` (HTTP).
- `fromTyradexJson()`, `fromDb()`, `toDb()`.
- **Datasources** :
- `PokemonLocalDataSource` — SQLite via sqflite ; absent (`null`) sur le web.
- `PokemonRemoteDataSource` — HTTP vers Tyradex + PokéAPI (genus anglais).
- **Repository (impl)** : `PokemonRepositoryImpl` applique « DB locale d'abord, sinon API + cache ».
Sur le web, le datasource local est absent (`null`).
### l10n
Internationalisation complète **FR / EN** via `flutter_localizations` + `intl`.
Fichiers générés : `app_localizations.dart`, `app_localizations_fr.dart`, `app_localizations_en.dart`.
La langue active est persistée dans SharedPreferences (`AppConstants.prefsLocale`).
### presentation
- **Providers** : `pokemonRepositoryProvider` (DI), `pokedexProvider` (`AsyncNotifier`),
`gameProvider` (`Notifier<GameState>`), `selectedTabProvider` (onglet courant).
- **Pages** : `ConsumerWidget` / `ConsumerStatefulWidget` qui observent les providers.
- **Widgets** : éléments réutilisables (`PokemonImage`, `PokemonTile`, `PokemonTypeWidget`).
- **Thème** : `type_colors.dart` (couleur/format des types).
#### Providers (Riverpod)
| Provider | Type | Rôle |
| --- | --- | --- |
| `pokemonRepositoryProvider` | `Provider` | DI du repository |
| `pokedexProvider` | `AsyncNotifier<List<Pokemon>>` | Chargement + cache du Pokédex |
| `gameProvider` | `Notifier<GameState>` | État de la partie en cours |
| `selectedTabProvider` | `StateProvider<int>` | Onglet de navigation courant |
| `localeProvider` | `Notifier<Locale>` | Langue active (FR/EN) + persistance |
| `themeProvider` | `Notifier<AppPalette>` | Palette de couleurs active (5 thèmes) + persistance |
| `genFilterProvider` | `Notifier<Set<int>>` | Générations sélectionnées pour le jeu + persistance |
| `caughtCountProvider` | `Provider<int>` | Nombre de Pokémon capturés (dérivé de pokedexProvider) |
#### Pages
| Fichier | Rôle |
| --- | --- |
| `main_page.dart` | Hub de navigation par onglets (Jeu / Pokédex / Système) |
| `guess_page.dart` | Écran principal du jeu (silhouette, input, lives, score) |
| `game_over_page.dart` | Écran de fin de partie avec stats de session |
| `pokemon_list.dart` | Pokédex : liste filtrée + recherche par nom |
| `pokemon_detail.dart` | Fiche détaillée : stats, type, genus, toggle normal/shiny |
| `system_page.dart` | Paramètres (langue, palette) et statistiques globales |
#### Widgets notables
- **Réutilisables** : `PokemonImage`, `PokemonTile`, `PokemonTypeWidget`, `ScanlineOverlay`.
- **guess/** : `GuessSilhouette`, `GuessInputSection`, `LivesRow`, `ScoreBoard`, `GenFilterSection`.
- **detail/** : `PokemonDetailTop`, `PokemonStatsPanel`.
- **game_over/** : `GameOverHeader`, `GameOverStats`, `GameOverActions`, `HingeDivider`.
- **list/** : `PokedexListHeader`, `PokedexCountBar`.
- **system/** : `LanguagePicker`, `PalettePicker`, `SectionTitle`, `SystemHeader`, `SystemStats`.
#### Thème
`type_colors.dart` : couleur et libellé localisé par type Pokémon.
## Flux de données
@@ -36,8 +87,19 @@ UI (Consumer) → Notifier → GameEngine (règles) + Repository (données)
Riverpod re-render automatiquement les consommateurs concernés. Plus de bus d'événements global
ni d'accès inter-pages via l'arbre de widgets.
## Mécaniques de jeu (résumé)
- **Vies** : 3 au départ ; une vie perdue par mauvaise réponse ; game over à 0.
- **Hints** : 3 au départ ; révèle partiellement le nom ; +1 tous les 5 bonnes réponses consécutives.
- **Skips** : 3 au départ ; passe au Pokémon suivant sans pénalité ; +1 tous les 10 bonnes réponses.
- **Score** : +10 pts normal, +20 pts shiny ; meilleur score persisté via SharedPreferences.
- **Shiny** : 1 chance sur 10 (`AppConstants.shinyOdds`).
- **Filtre de génération** : le joueur choisit parmi Gen IIX ; persisté entre les sessions.
- **Acceptation bilingue** : le nom FR *ou* EN est accepté quelle que soit la langue de l'UI.
## Tests
- `test/domain/game_engine_test.dart` : règles du jeu.
- `test/data/pokemon_dto_test.dart` : parsing.
- `test/data/pokemon_dto_test.dart` : parsing JSON ↔ DB.
- `test/data/pokemon_repository_test.dart` : logique du repository (datasources factices).
- `test/widget_test.dart` : smoke test de démarrage de l'app.
+26 -11
View File
@@ -2,15 +2,27 @@
## Description
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokemon through a silhouette guessing game. The app fetch data from the Tyradex API and stores it locally for offline access.
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokémon through
a silhouette guessing game. The app fetches data from the Tyradex API and stores it locally for
offline access.
## Features
- National Pokedex: Browse all 1025+ Pokemon from all generations.
- Guess Game: Identify Pokemon by their silhouette.
- Scoring System: Earn points for correct guesses, with bonuses for Shiny Pokemon. High scores are saved locally.
- Collection: Track caught and seen Pokemon.
- Search and Filter: Filter the collection by all or caught status and search by name.
- **National Pokédex**: Browse all 1025 Pokémon from generations I to IX.
- **Guess Game**: Identify Pokémon by their silhouette.
- **Scoring System**: Earn 10 pts per correct guess, 20 pts for Shiny Pokémon (1 in 10 chance).
High scores are saved locally.
- **Lives, Hints & Skips**: Start each game with 3 lives, 3 hints, and 3 skips. Earn bonuses
every 5 correct guesses (hint) and every 10 correct guesses (skip).
- **Generation Filter**: Restrict the game to one or more generations (IIX). Selection persists
across sessions.
- **Bilingual Input**: Pokémon names are accepted in French or English regardless of the UI language.
- **Collection**: Track caught and seen Pokémon across your Pokédex.
- **Search and Filter**: Filter the collection by all / caught status and search by name.
- **Pokémon Detail**: View base stats, types, genus, and toggle between normal and shiny sprites.
- **Theming**: Choose from 5 color palettes (red, blue, green, yellow, purple).
- **Localization**: Full French and English UI support, switchable at any time.
- **Statistics**: Global view of caught count, seen count, completion percentage, and best score.
## Installation
@@ -21,8 +33,11 @@ Pokeguess is a Flutter mobile application that allows users to discover and coll
## Technologies
- Flutter: UI Framework.
- SQLite (sqflite): Local database.
- Tyradex API: Pokemon data source.
- Shared Preferences: High score persistence.
- Google Fonts: Custom typography.
- **Flutter**: UI Framework.
- **Riverpod**: State management (providers + notifiers).
- **SQLite (sqflite)**: Local database for offline-first Pokémon storage.
- **Tyradex API**: Primary Pokémon data source (FR/EN names, sprites, types, stats).
- **PokéAPI**: Secondary source for English genus data.
- **Shared Preferences**: Persistence for best score, generation filter, language, and palette.
- **Google Fonts**: Custom typography (VT323).
- **flutter_localizations / intl**: FR and EN localization.
+4
View File
@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n
+3
View File
@@ -41,6 +41,9 @@ class AppConstants {
/// Clé SharedPreferences pour le filtre de générations.
static const String prefsGenFilter = 'gen_filter';
/// Clé SharedPreferences pour la langue de l'application.
static const String prefsLocale = 'locale_code';
/// Plages d'IDs Pokémon par génération [min, max] (inclusif).
static const List<(int, int)> genRanges = [
(1, 151), // Gen 1
@@ -2,24 +2,43 @@ import 'package:sqflite_common/sqflite.dart';
import '../../domain/entities/pokemon.dart';
import '../dto/pokemon_dto.dart';
/// Accès SQLite local au Pokédex. Schéma et migrations identiques à l'ancien PokedexDatabase.
const _createSql = '''
CREATE TABLE IF NOT EXISTS pokemon (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
name_fr TEXT,
name_en TEXT,
type1 TEXT NOT NULL,
type2 TEXT,
hp INTEGER NOT NULL,
atk INTEGER NOT NULL,
def INTEGER NOT NULL,
spd INTEGER NOT NULL,
description TEXT,
description_en TEXT,
isCaught INTEGER NOT NULL DEFAULT 0,
isSeen INTEGER NOT NULL DEFAULT 0
)
''';
class PokemonLocalDataSource {
final String languageCode;
PokemonLocalDataSource({this.languageCode = 'fr'});
Future<Database>? _dbFuture;
Future<Database> _getDb() {
return _dbFuture ??= openDatabase(
'pokedex.db',
version: 2,
version: 5,
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute('DROP TABLE IF EXISTS pokemon');
await db.execute(
'CREATE TABLE pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
}
// Drop and recreate on any upgrade to pick up new columns cleanly.
await db.execute('DROP TABLE IF EXISTS pokemon');
await db.execute(_createSql);
},
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
await db.execute(_createSql);
},
);
}
@@ -27,14 +46,14 @@ class PokemonLocalDataSource {
Future<List<Pokemon>> getAll() async {
final db = await _getDb();
final rows = await db.query('pokemon');
return rows.map(PokemonDto.fromDb).toList();
return rows.map((r) => PokemonDto.fromDb(r, languageCode: languageCode)).toList();
}
Future<Pokemon?> getById(int id) async {
final db = await _getDb();
final rows = await db.query('pokemon', where: 'id = ?', whereArgs: [id]);
if (rows.isEmpty) return null;
return PokemonDto.fromDb(rows.first);
return PokemonDto.fromDb(rows.first, languageCode: languageCode);
}
Future<void> saveAll(List<Pokemon> pokemons) async {
@@ -8,12 +8,13 @@ import '../dto/pokemon_dto.dart';
/// Accès distant à l'API Tyradex.
class PokemonRemoteDataSource {
final http.Client _client;
final String languageCode;
PokemonRemoteDataSource({http.Client? client})
PokemonRemoteDataSource({http.Client? client, this.languageCode = 'fr'})
: _client = client ?? http.Client();
Future<Pokemon> getById(int id) async {
AppLogger.info('API: fetching Pokémon $id');
AppLogger.info('API: fetching Pokémon $id (lang: $languageCode)');
final response = await _client
.get(Uri.https(AppConstants.apiBaseUrl, '${AppConstants.apiPokemonPath}/$id'));
if (response.statusCode != 200) {
@@ -21,11 +22,29 @@ class PokemonRemoteDataSource {
'Erreur récupération du pokémon $id, code ${response.statusCode}');
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
return PokemonDto.fromTyradexJson(json, fallbackId: id);
return PokemonDto.fromTyradexJson(json,
fallbackId: id, languageCode: languageCode);
}
/// Fetches the English genus (category) from PokéAPI for a given Pokémon id.
/// Returns null on any error (network, missing entry, etc.).
Future<String?> getEnglishGenus(int id) async {
try {
final response = await _client
.get(Uri.https('pokeapi.co', '/api/v2/pokemon-species/$id/'));
if (response.statusCode != 200) return null;
final json = jsonDecode(response.body) as Map<String, dynamic>;
final genera = json['genera'] as List<dynamic>? ?? [];
for (final g in genera) {
final lang = (g['language'] as Map<String, dynamic>?)?['name'];
if (lang == 'en') return g['genus'] as String?;
}
} catch (_) {}
return null;
}
Future<List<Pokemon>> getAll() async {
AppLogger.info('API: fetching ALL Pokémon');
AppLogger.info('API: fetching ALL Pokémon (lang: $languageCode)');
final response = await _client
.get(Uri.https(AppConstants.apiBaseUrl, AppConstants.apiPokemonPath));
if (response.statusCode != 200) {
@@ -34,9 +53,12 @@ class PokemonRemoteDataSource {
final List<dynamic> jsonList = jsonDecode(response.body);
final result = <Pokemon>[];
for (final json in jsonList) {
if (json['pokedex_id'] == 0) continue; // entrée générique Tyradex
if (json['pokedex_id'] == 0) continue;
try {
result.add(PokemonDto.fromTyradexJson(json as Map<String, dynamic>));
result.add(PokemonDto.fromTyradexJson(
json as Map<String, dynamic>,
languageCode: languageCode,
));
} catch (e, st) {
AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st);
}
+47 -15
View File
@@ -4,8 +4,7 @@ import '../../domain/entities/pokemon.dart';
class PokemonDto {
PokemonDto._();
/// Mappe un nom de type français (API Tyradex) vers l'enum.
static PokemonType frenchTypeToEnum(String frenchType) {
static PokemonType _frenchTypeToEnum(String type) {
const map = {
'Normal': PokemonType.normal,
'Combat': PokemonType.fighting,
@@ -26,41 +25,65 @@ class PokemonDto {
'Ténèbres': PokemonType.dark,
'Fée': PokemonType.fairy,
};
return map[frenchType] ?? PokemonType.unknown;
return map[type] ?? PokemonType.unknown;
}
/// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste).
/// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`.
static Pokemon fromTyradexJson(Map<String, dynamic> json, {int? fallbackId}) {
// Tyradex always returns type names in French regardless of endpoint language.
static PokemonType _typeToEnum(String typeName, String languageCode) {
return _frenchTypeToEnum(typeName);
}
/// Construit une entité depuis la réponse Tyradex.
/// [languageCode] détermine quelle langue utiliser pour [name] ('fr' ou 'en').
static Pokemon fromTyradexJson(
Map<String, dynamic> json, {
int? fallbackId,
String languageCode = 'fr',
}) {
final id = (json['pokedex_id'] as int?) ??
fallbackId ??
(throw ArgumentError('pokedex_id absent et fallbackId non fourni'));
final nameMap = json['name'] as Map<String, dynamic>?;
final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown';
final nameFr = nameMap?['fr'] as String? ?? 'unknown';
final nameEn = nameMap?['en'] as String? ?? nameFr;
final name = languageCode == 'en' ? nameEn : nameFr;
final List types = json['types'] ?? [];
final type1 =
types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown;
final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null;
// Tyradex retourne toujours les types dans la langue du endpoint (fr ou en).
final type1 = types.isNotEmpty
? _typeToEnum(types[0]['name'] as String, languageCode)
: PokemonType.unknown;
final type2 = types.length > 1
? _typeToEnum(types[1]['name'] as String, languageCode)
: null;
final Map<String, dynamic>? stats = json['stats'];
// Tyradex expose 'category' (FR) et parfois 'category_en' (EN).
final descFr = json['category'] as String?;
final descEn = (json['category_en'] ?? json['category']) as String?;
return Pokemon(
name: name,
nameFr: nameFr,
nameEn: nameEn,
id: id,
type1: type1,
type2: type2,
hp: stats?['hp'] ?? 0,
atk: stats?['atk'] ?? 0,
def: stats?['def'] ?? 0,
spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex
description: json['category'],
spd: stats?['vit'] ?? 0,
description: descFr,
descriptionEn: descEn,
);
}
/// Sérialise pour SQLite.
/// Sérialise pour SQLite (stocke les deux noms et descriptions).
static Map<String, dynamic> toDb(Pokemon p) {
return {
'name': p.name,
'name_fr': p.nameFr ?? p.name,
'name_en': p.nameEn ?? p.name,
'id': p.id,
'type1': p.type1.name,
'type2': p.type2?.name,
@@ -69,15 +92,23 @@ class PokemonDto {
'def': p.def,
'spd': p.spd,
'description': p.description,
'description_en': p.descriptionEn,
'isCaught': p.isCaught ? 1 : 0,
'isSeen': p.isSeen ? 1 : 0,
};
}
/// Reconstruit depuis une ligne SQLite.
static Pokemon fromDb(Map<String, dynamic> row) {
/// [languageCode] détermine quel nom afficher.
static Pokemon fromDb(Map<String, dynamic> row, {String languageCode = 'fr'}) {
final nameFr = row['name_fr'] as String? ?? row['name'] as String;
final nameEn = row['name_en'] as String? ?? nameFr;
final name = languageCode == 'en' ? nameEn : nameFr;
return Pokemon(
name: row['name'],
name: name,
nameFr: nameFr,
nameEn: nameEn,
id: row['id'],
type1: PokemonType.values.firstWhere(
(e) => e.name == row['type1'],
@@ -94,6 +125,7 @@ class PokemonDto {
def: row['def'] ?? 0,
spd: row['spd'] ?? 0,
description: row['description'],
descriptionEn: row['description_en'] as String?,
isCaught: row['isCaught'] == 1 || row['isCaught'] == true,
isSeen: row['isSeen'] == 1 || row['isSeen'] == true,
);
@@ -60,4 +60,7 @@ class PokemonRepositoryImpl implements PokemonRepository {
@override
Future<int> seenCount() async => (await local?.seenCount()) ?? 0;
@override
Future<String?> getEnglishGenus(int id) => remote.getEnglishGenus(id);
}
+12
View File
@@ -1,6 +1,8 @@
/// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API.
class Pokemon {
final String name;
final String? nameFr;
final String? nameEn;
final int id;
final PokemonType type1;
final PokemonType? type2;
@@ -9,11 +11,14 @@ class Pokemon {
final int def;
final int spd;
final String? description;
final String? descriptionEn;
final bool isCaught;
final bool isSeen;
const Pokemon({
required this.name,
this.nameFr,
this.nameEn,
required this.id,
required this.type1,
this.type2,
@@ -22,6 +27,7 @@ class Pokemon {
required this.def,
required this.spd,
this.description,
this.descriptionEn,
this.isCaught = false,
this.isSeen = false,
});
@@ -37,6 +43,8 @@ class Pokemon {
Pokemon copyWith({
String? name,
String? nameFr,
String? nameEn,
int? id,
PokemonType? type1,
PokemonType? type2,
@@ -45,11 +53,14 @@ class Pokemon {
int? def,
int? spd,
String? description,
String? descriptionEn,
bool? isCaught,
bool? isSeen,
}) {
return Pokemon(
name: name ?? this.name,
nameFr: nameFr ?? this.nameFr,
nameEn: nameEn ?? this.nameEn,
id: id ?? this.id,
type1: type1 ?? this.type1,
type2: type2 ?? this.type2,
@@ -58,6 +69,7 @@ class Pokemon {
def: def ?? this.def,
spd: spd ?? this.spd,
description: description ?? this.description,
descriptionEn: descriptionEn ?? this.descriptionEn,
isCaught: isCaught ?? this.isCaught,
isSeen: isSeen ?? this.isSeen,
);
+8 -2
View File
@@ -37,9 +37,15 @@ class GameEngine {
}
final normalizedGuess = _normalize(guess.trim().toLowerCase());
final normalizedActual = _normalize(pokemon.name.toLowerCase());
if (normalizedGuess == normalizedActual) {
// Accept the name in either language so a language switch mid-game never blocks the player.
final acceptedNames = <String>{
pokemon.name,
if (pokemon.nameFr != null) pokemon.nameFr!,
if (pokemon.nameEn != null) pokemon.nameEn!,
}.map((n) => _normalize(n.toLowerCase())).toSet();
if (acceptedNames.contains(normalizedGuess)) {
final newSession = s.sessionCorrectCount + 1;
final gained = s.isShiny ? AppConstants.pointsShiny : AppConstants.pointsNormal;
final newScore = s.currentScore + gained;
@@ -19,4 +19,7 @@ abstract interface class PokemonRepository {
/// Nombre de Pokémon vus.
Future<int> seenCount();
/// Catégorie anglaise d'un Pokémon (ex. "Seed Pokémon") depuis PokéAPI. Null si indisponible.
Future<String?> getEnglishGenus(int id);
}
+70
View File
@@ -0,0 +1,70 @@
{
"@@locale": "en",
"appTitle": "Pokéguess",
"whosThatPokemon": "WHO'S THAT POKÉMON?",
"shinyDetected": "✨ SHINY POKÉMON DETECTED! ✨",
"genFilter": "GEN FILTER",
"identificationInput": "IDENTIFICATION INPUT",
"hintPrefix": "HINT",
"enterPokemonName": "Enter Pokémon name...",
"continueButton": "CONTINUE",
"guessButton": "GUESS!",
"hintButton": "HINT ({count})",
"@hintButton": { "placeholders": { "count": { "type": "int" } } },
"skipButton": "SKIP ({count})",
"@skipButton": { "placeholders": { "count": { "type": "int" } } },
"currentScore": "CURRENT SCORE",
"personalBest": "PERSONAL BEST: {score}",
"@personalBest": { "placeholders": { "score": { "type": "int" } } },
"caughtShiny": "✨ SHINY! You caught {name}! (+20 pts) ✨",
"@caughtShiny": { "placeholders": { "name": { "type": "String" } } },
"caughtNormal": "Correct! You caught {name}!",
"@caughtNormal": { "placeholders": { "name": { "type": "String" } } },
"wrongGuess": "Wrong guess! Try again.",
"errorLoadingPokemon": "Error loading Pokémon",
"listNational": "LIST - NATIONAL",
"tabAll": "ALL",
"tabCaught": "CAUGHT",
"pokemonDiscovered": "POKEMON DISCOVERED",
"noPokemonFound": "NO POKEMON FOUND IN {filter}",
"@noPokemonFound": { "placeholders": { "filter": { "type": "String" } } },
"pokedexFooter": "NATIONAL POKEDEX V2.0",
"loadingError": "Loading error",
"baseStats": "BASE STATS",
"noDescription": "No description available for this Pokémon.",
"gameOver": "GAME OVER",
"itWas": "It was {name}!",
"@itWas": { "placeholders": { "name": { "type": "String" } } },
"gameOverMessage": "\"Looks like your journey ends here. You've run out of energy!\"",
"statStreak": "STREAK",
"statSeen": "SEEN",
"statScore": "SCORE",
"tryAgain": "TRY AGAIN",
"backToPokedex": "BACK TO POKEDEX",
"system": "SYSTEM",
"statistics": "STATISTICS",
"colorPalette": "COLOR PALETTE",
"language": "LANGUAGE",
"statBestScore": "Best score",
"statCaught": "Caught",
"statSeenLabel": "Seen",
"statCompletion": "Completion",
"navList": "LIST",
"navGuess": "GUESS",
"navSystem": "SYSTEM",
"langEnglish": "English",
"langFrench": "French",
"palette0": "Pokédex Red",
"palette1": "Blue Ocean",
"palette2": "Green Forest",
"palette3": "Lightning Yellow",
"palette4": "Purple Shadow"
}
+63
View File
@@ -0,0 +1,63 @@
{
"@@locale": "fr",
"appTitle": "Pokéguess",
"whosThatPokemon": "QUI EST CE POKÉMON ?",
"shinyDetected": "✨ POKÉMON SHINY DÉTECTÉ ! ✨",
"genFilter": "FILTRE GÉN.",
"identificationInput": "SAISIE D'IDENTIFICATION",
"hintPrefix": "INDICE",
"enterPokemonName": "Entrez le nom du Pokémon...",
"continueButton": "CONTINUER",
"guessButton": "DEVINER !",
"hintButton": "INDICE ({count})",
"skipButton": "PASSER ({count})",
"currentScore": "SCORE ACTUEL",
"personalBest": "MEILLEUR SCORE : {score}",
"caughtShiny": "✨ SHINY ! Tu as attrapé {name} ! (+20 pts) ✨",
"caughtNormal": "Correct ! Tu as attrapé {name} !",
"wrongGuess": "Mauvaise réponse ! Réessaie.",
"errorLoadingPokemon": "Erreur de chargement du Pokémon",
"listNational": "LISTE - NATIONALE",
"tabAll": "TOUS",
"tabCaught": "ATTRAPÉS",
"pokemonDiscovered": "POKÉMON DÉCOUVERTS",
"noPokemonFound": "AUCUN POKÉMON DANS {filter}",
"pokedexFooter": "POKÉDEX NATIONAL V2.0",
"loadingError": "Erreur de chargement",
"baseStats": "STATS DE BASE",
"noDescription": "Aucune description disponible pour ce Pokémon.",
"gameOver": "PARTIE TERMINÉE",
"itWas": "C'était {name} !",
"gameOverMessage": "« On dirait que ton aventure s'arrête ici. Tu n'as plus d'énergie ! »",
"statStreak": "SÉRIE",
"statSeen": "VUS",
"statScore": "SCORE",
"tryAgain": "REJOUER",
"backToPokedex": "RETOUR AU POKÉDEX",
"system": "SYSTÈME",
"statistics": "STATISTIQUES",
"colorPalette": "PALETTE DE COULEURS",
"language": "LANGUE",
"statBestScore": "Meilleur score",
"statCaught": "Attrapés",
"statSeenLabel": "Vus",
"statCompletion": "Complétion",
"navList": "LISTE",
"navGuess": "DEVINER",
"navSystem": "SYSTÈME",
"langEnglish": "Anglais",
"langFrench": "Français",
"palette0": "Pokédex Rouge",
"palette1": "Océan Bleu",
"palette2": "Forêt Verte",
"palette3": "Foudre Jaune",
"palette4": "Ombre Violette"
}
+445
View File
@@ -0,0 +1,445 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_fr.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('fr')
];
/// No description provided for @appTitle.
///
/// In en, this message translates to:
/// **'Pokéguess'**
String get appTitle;
/// No description provided for @whosThatPokemon.
///
/// In en, this message translates to:
/// **'WHO\'S THAT POKÉMON?'**
String get whosThatPokemon;
/// No description provided for @shinyDetected.
///
/// In en, this message translates to:
/// **'✨ SHINY POKÉMON DETECTED! ✨'**
String get shinyDetected;
/// No description provided for @genFilter.
///
/// In en, this message translates to:
/// **'GEN FILTER'**
String get genFilter;
/// No description provided for @identificationInput.
///
/// In en, this message translates to:
/// **'IDENTIFICATION INPUT'**
String get identificationInput;
/// No description provided for @hintPrefix.
///
/// In en, this message translates to:
/// **'HINT'**
String get hintPrefix;
/// No description provided for @enterPokemonName.
///
/// In en, this message translates to:
/// **'Enter Pokémon name...'**
String get enterPokemonName;
/// No description provided for @continueButton.
///
/// In en, this message translates to:
/// **'CONTINUE'**
String get continueButton;
/// No description provided for @guessButton.
///
/// In en, this message translates to:
/// **'GUESS!'**
String get guessButton;
/// No description provided for @hintButton.
///
/// In en, this message translates to:
/// **'HINT ({count})'**
String hintButton(int count);
/// No description provided for @skipButton.
///
/// In en, this message translates to:
/// **'SKIP ({count})'**
String skipButton(int count);
/// No description provided for @currentScore.
///
/// In en, this message translates to:
/// **'CURRENT SCORE'**
String get currentScore;
/// No description provided for @personalBest.
///
/// In en, this message translates to:
/// **'PERSONAL BEST: {score}'**
String personalBest(int score);
/// No description provided for @caughtShiny.
///
/// In en, this message translates to:
/// **'✨ SHINY! You caught {name}! (+20 pts) ✨'**
String caughtShiny(String name);
/// No description provided for @caughtNormal.
///
/// In en, this message translates to:
/// **'Correct! You caught {name}!'**
String caughtNormal(String name);
/// No description provided for @wrongGuess.
///
/// In en, this message translates to:
/// **'Wrong guess! Try again.'**
String get wrongGuess;
/// No description provided for @errorLoadingPokemon.
///
/// In en, this message translates to:
/// **'Error loading Pokémon'**
String get errorLoadingPokemon;
/// No description provided for @listNational.
///
/// In en, this message translates to:
/// **'LIST - NATIONAL'**
String get listNational;
/// No description provided for @tabAll.
///
/// In en, this message translates to:
/// **'ALL'**
String get tabAll;
/// No description provided for @tabCaught.
///
/// In en, this message translates to:
/// **'CAUGHT'**
String get tabCaught;
/// No description provided for @pokemonDiscovered.
///
/// In en, this message translates to:
/// **'POKEMON DISCOVERED'**
String get pokemonDiscovered;
/// No description provided for @noPokemonFound.
///
/// In en, this message translates to:
/// **'NO POKEMON FOUND IN {filter}'**
String noPokemonFound(String filter);
/// No description provided for @pokedexFooter.
///
/// In en, this message translates to:
/// **'NATIONAL POKEDEX V2.0'**
String get pokedexFooter;
/// No description provided for @loadingError.
///
/// In en, this message translates to:
/// **'Loading error'**
String get loadingError;
/// No description provided for @baseStats.
///
/// In en, this message translates to:
/// **'BASE STATS'**
String get baseStats;
/// No description provided for @noDescription.
///
/// In en, this message translates to:
/// **'No description available for this Pokémon.'**
String get noDescription;
/// No description provided for @gameOver.
///
/// In en, this message translates to:
/// **'GAME OVER'**
String get gameOver;
/// No description provided for @itWas.
///
/// In en, this message translates to:
/// **'It was {name}!'**
String itWas(String name);
/// No description provided for @gameOverMessage.
///
/// In en, this message translates to:
/// **'\"Looks like your journey ends here. You\'ve run out of energy!\"'**
String get gameOverMessage;
/// No description provided for @statStreak.
///
/// In en, this message translates to:
/// **'STREAK'**
String get statStreak;
/// No description provided for @statSeen.
///
/// In en, this message translates to:
/// **'SEEN'**
String get statSeen;
/// No description provided for @statScore.
///
/// In en, this message translates to:
/// **'SCORE'**
String get statScore;
/// No description provided for @tryAgain.
///
/// In en, this message translates to:
/// **'TRY AGAIN'**
String get tryAgain;
/// No description provided for @backToPokedex.
///
/// In en, this message translates to:
/// **'BACK TO POKEDEX'**
String get backToPokedex;
/// No description provided for @system.
///
/// In en, this message translates to:
/// **'SYSTEM'**
String get system;
/// No description provided for @statistics.
///
/// In en, this message translates to:
/// **'STATISTICS'**
String get statistics;
/// No description provided for @colorPalette.
///
/// In en, this message translates to:
/// **'COLOR PALETTE'**
String get colorPalette;
/// No description provided for @language.
///
/// In en, this message translates to:
/// **'LANGUAGE'**
String get language;
/// No description provided for @statBestScore.
///
/// In en, this message translates to:
/// **'Best score'**
String get statBestScore;
/// No description provided for @statCaught.
///
/// In en, this message translates to:
/// **'Caught'**
String get statCaught;
/// No description provided for @statSeenLabel.
///
/// In en, this message translates to:
/// **'Seen'**
String get statSeenLabel;
/// No description provided for @statCompletion.
///
/// In en, this message translates to:
/// **'Completion'**
String get statCompletion;
/// No description provided for @navList.
///
/// In en, this message translates to:
/// **'LIST'**
String get navList;
/// No description provided for @navGuess.
///
/// In en, this message translates to:
/// **'GUESS'**
String get navGuess;
/// No description provided for @navSystem.
///
/// In en, this message translates to:
/// **'SYSTEM'**
String get navSystem;
/// No description provided for @langEnglish.
///
/// In en, this message translates to:
/// **'English'**
String get langEnglish;
/// No description provided for @langFrench.
///
/// In en, this message translates to:
/// **'French'**
String get langFrench;
/// No description provided for @palette0.
///
/// In en, this message translates to:
/// **'Pokédex Red'**
String get palette0;
/// No description provided for @palette1.
///
/// In en, this message translates to:
/// **'Blue Ocean'**
String get palette1;
/// No description provided for @palette2.
///
/// In en, this message translates to:
/// **'Green Forest'**
String get palette2;
/// No description provided for @palette3.
///
/// In en, this message translates to:
/// **'Lightning Yellow'**
String get palette3;
/// No description provided for @palette4.
///
/// In en, this message translates to:
/// **'Purple Shadow'**
String get palette4;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'fr'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'fr':
return AppLocalizationsFr();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
}
+181
View File
@@ -0,0 +1,181 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'Pokéguess';
@override
String get whosThatPokemon => 'WHO\'S THAT POKÉMON?';
@override
String get shinyDetected => '✨ SHINY POKÉMON DETECTED! ✨';
@override
String get genFilter => 'GEN FILTER';
@override
String get identificationInput => 'IDENTIFICATION INPUT';
@override
String get hintPrefix => 'HINT';
@override
String get enterPokemonName => 'Enter Pokémon name...';
@override
String get continueButton => 'CONTINUE';
@override
String get guessButton => 'GUESS!';
@override
String hintButton(int count) {
return 'HINT ($count)';
}
@override
String skipButton(int count) {
return 'SKIP ($count)';
}
@override
String get currentScore => 'CURRENT SCORE';
@override
String personalBest(int score) {
return 'PERSONAL BEST: $score';
}
@override
String caughtShiny(String name) {
return '✨ SHINY! You caught $name! (+20 pts) ✨';
}
@override
String caughtNormal(String name) {
return 'Correct! You caught $name!';
}
@override
String get wrongGuess => 'Wrong guess! Try again.';
@override
String get errorLoadingPokemon => 'Error loading Pokémon';
@override
String get listNational => 'LIST - NATIONAL';
@override
String get tabAll => 'ALL';
@override
String get tabCaught => 'CAUGHT';
@override
String get pokemonDiscovered => 'POKEMON DISCOVERED';
@override
String noPokemonFound(String filter) {
return 'NO POKEMON FOUND IN $filter';
}
@override
String get pokedexFooter => 'NATIONAL POKEDEX V2.0';
@override
String get loadingError => 'Loading error';
@override
String get baseStats => 'BASE STATS';
@override
String get noDescription => 'No description available for this Pokémon.';
@override
String get gameOver => 'GAME OVER';
@override
String itWas(String name) {
return 'It was $name!';
}
@override
String get gameOverMessage =>
'\"Looks like your journey ends here. You\'ve run out of energy!\"';
@override
String get statStreak => 'STREAK';
@override
String get statSeen => 'SEEN';
@override
String get statScore => 'SCORE';
@override
String get tryAgain => 'TRY AGAIN';
@override
String get backToPokedex => 'BACK TO POKEDEX';
@override
String get system => 'SYSTEM';
@override
String get statistics => 'STATISTICS';
@override
String get colorPalette => 'COLOR PALETTE';
@override
String get language => 'LANGUAGE';
@override
String get statBestScore => 'Best score';
@override
String get statCaught => 'Caught';
@override
String get statSeenLabel => 'Seen';
@override
String get statCompletion => 'Completion';
@override
String get navList => 'LIST';
@override
String get navGuess => 'GUESS';
@override
String get navSystem => 'SYSTEM';
@override
String get langEnglish => 'English';
@override
String get langFrench => 'French';
@override
String get palette0 => 'Pokédex Red';
@override
String get palette1 => 'Blue Ocean';
@override
String get palette2 => 'Green Forest';
@override
String get palette3 => 'Lightning Yellow';
@override
String get palette4 => 'Purple Shadow';
}
+181
View File
@@ -0,0 +1,181 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'Pokéguess';
@override
String get whosThatPokemon => 'QUI EST CE POKÉMON ?';
@override
String get shinyDetected => '✨ POKÉMON SHINY DÉTECTÉ ! ✨';
@override
String get genFilter => 'FILTRE GÉN.';
@override
String get identificationInput => 'SAISIE D\'IDENTIFICATION';
@override
String get hintPrefix => 'INDICE';
@override
String get enterPokemonName => 'Entrez le nom du Pokémon...';
@override
String get continueButton => 'CONTINUER';
@override
String get guessButton => 'DEVINER !';
@override
String hintButton(int count) {
return 'INDICE ($count)';
}
@override
String skipButton(int count) {
return 'PASSER ($count)';
}
@override
String get currentScore => 'SCORE ACTUEL';
@override
String personalBest(int score) {
return 'MEILLEUR SCORE : $score';
}
@override
String caughtShiny(String name) {
return '✨ SHINY ! Tu as attrapé $name ! (+20 pts) ✨';
}
@override
String caughtNormal(String name) {
return 'Correct ! Tu as attrapé $name !';
}
@override
String get wrongGuess => 'Mauvaise réponse ! Réessaie.';
@override
String get errorLoadingPokemon => 'Erreur de chargement du Pokémon';
@override
String get listNational => 'LISTE - NATIONALE';
@override
String get tabAll => 'TOUS';
@override
String get tabCaught => 'ATTRAPÉS';
@override
String get pokemonDiscovered => 'POKÉMON DÉCOUVERTS';
@override
String noPokemonFound(String filter) {
return 'AUCUN POKÉMON DANS $filter';
}
@override
String get pokedexFooter => 'POKÉDEX NATIONAL V2.0';
@override
String get loadingError => 'Erreur de chargement';
@override
String get baseStats => 'STATS DE BASE';
@override
String get noDescription => 'Aucune description disponible pour ce Pokémon.';
@override
String get gameOver => 'PARTIE TERMINÉE';
@override
String itWas(String name) {
return 'C\'était $name !';
}
@override
String get gameOverMessage =>
'« On dirait que ton aventure s\'arrête ici. Tu n\'as plus d\'énergie ! »';
@override
String get statStreak => 'SÉRIE';
@override
String get statSeen => 'VUS';
@override
String get statScore => 'SCORE';
@override
String get tryAgain => 'REJOUER';
@override
String get backToPokedex => 'RETOUR AU POKÉDEX';
@override
String get system => 'SYSTÈME';
@override
String get statistics => 'STATISTIQUES';
@override
String get colorPalette => 'PALETTE DE COULEURS';
@override
String get language => 'LANGUE';
@override
String get statBestScore => 'Meilleur score';
@override
String get statCaught => 'Attrapés';
@override
String get statSeenLabel => 'Vus';
@override
String get statCompletion => 'Complétion';
@override
String get navList => 'LISTE';
@override
String get navGuess => 'DEVINER';
@override
String get navSystem => 'SYSTÈME';
@override
String get langEnglish => 'Anglais';
@override
String get langFrench => 'Français';
@override
String get palette0 => 'Pokédex Rouge';
@override
String get palette1 => 'Océan Bleu';
@override
String get palette2 => 'Forêt Verte';
@override
String get palette3 => 'Foudre Jaune';
@override
String get palette4 => 'Ombre Violette';
}
+7 -1
View File
@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'l10n/app_localizations.dart';
import 'presentation/pages/main_page.dart';
import 'presentation/pages/pokemon_detail.dart';
import 'presentation/pages/game_over_page.dart';
import 'presentation/providers/theme_provider.dart';
import 'presentation/providers/locale_provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
@@ -25,9 +27,13 @@ class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex];
final locale = ref.watch(localeProvider);
return MaterialApp(
title: 'Pokéguess',
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: palette.primary,
+21 -10
View File
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/repository_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/game_over/game_over_header.dart';
import '../widgets/game_over/game_over_stats.dart';
import '../widgets/game_over/game_over_actions.dart';
@@ -43,12 +45,14 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
final int streak = args?['streak'] ?? 0;
final int score = args?['score'] ?? 0;
const darkRed = Color(0xFF9E1B1B);
final palette = appPalettes[ref.watch(themeProvider)];
final primaryDark = HSLColor.fromColor(palette.primary)
.withLightness((HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0))
.toColor();
const silverBg = Color(0xFFC8D1D8);
const messageBoxBg = Color(0xFF1B2333);
return Scaffold(
backgroundColor: const Color(0xFFD32F2F),
backgroundColor: palette.primary,
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: SingleChildScrollView(
@@ -56,10 +60,15 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
GameOverHeader(pokemonImage: pokemonImage, pokemonName: pokemonName),
const HingeDivider(),
GameOverHeader(
pokemonImage: pokemonImage,
pokemonName: pokemonName,
primaryDark: primaryDark,
surfaceColor: palette.surface,
),
HingeDivider(color: primaryDark),
Container(
decoration: BoxDecoration(color: darkRed, border: Border.all(color: darkRed, width: 4)),
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
@@ -68,12 +77,12 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
children: [
Container(
width: double.infinity,
color: messageBoxBg,
color: palette.surface,
padding: const EdgeInsets.all(24),
child: const Text(
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
child: Text(
AppLocalizations.of(context)!.gameOverMessage,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
style: const TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
),
),
const SizedBox(height: 16),
@@ -84,6 +93,8 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
),
const SizedBox(height: 16),
GameOverActions(
primaryColor: palette.primary,
primaryDark: primaryDark,
onTryAgain: () => Navigator.pop(context, true),
onBack: () => Navigator.pop(context, false),
),
+11 -7
View File
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/game/game_state.dart';
import '../../l10n/app_localizations.dart';
import '../providers/gen_filter_provider.dart';
import '../providers/game_provider.dart';
import '../providers/navigation_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/guess/guess_silhouette.dart';
import '../widgets/guess/gen_filter_section.dart';
@@ -44,19 +46,19 @@ class _GuessPageState extends ConsumerState<GuessPage> {
Future<void> _onGuess() async {
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
if (!mounted) return;
final l = AppLocalizations.of(context)!;
final state = ref.read(gameProvider);
switch (result) {
case GuessResult.correct:
final name = state.currentPokemon!.formatedName;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.isShiny
? '✨ SHINY! You caught ${state.currentPokemon!.formatedName}! (+20 pts) ✨'
: 'Correct! You caught ${state.currentPokemon!.formatedName}!'),
content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)),
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
));
break;
case GuessResult.wrong:
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(l.wrongGuess), backgroundColor: Colors.orange));
break;
case GuessResult.gameOver:
await _showGameOver();
@@ -91,12 +93,13 @@ class _GuessPageState extends ConsumerState<GuessPage> {
@override
Widget build(BuildContext context) {
final state = ref.watch(gameProvider);
final palette = appPalettes[ref.watch(themeProvider)];
if (state.status == GameStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.currentPokemon == null || state.status == GameStatus.error) {
return const Center(child: Text("Error loading Pokémon"));
return Center(child: Text(AppLocalizations.of(context)!.errorLoadingPokemon));
}
final pokemon = state.currentPokemon!;
@@ -111,12 +114,13 @@ class _GuessPageState extends ConsumerState<GuessPage> {
SingleChildScrollView(
child: Column(
children: [
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed),
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed, surfaceColor: palette.surface),
GenFilterSection(
isOpen: _genFilterOpen,
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
selectedGens: ref.watch(genFilterProvider),
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
surfaceColor: palette.surface,
),
const SizedBox(height: 12),
LivesRow(lives: state.lives),
+5 -4
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../l10n/app_localizations.dart';
import '../providers/navigation_provider.dart';
import '../providers/theme_provider.dart';
import 'pokemon_list.dart';
@@ -56,10 +57,10 @@ class MainPage extends ConsumerWidget {
type: BottomNavigationBarType.fixed,
selectedItemColor: palette.primary,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'LIST'),
BottomNavigationBarItem(icon: Icon(Icons.games), label: 'GUESS'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'SYSTEM'),
items: [
BottomNavigationBarItem(icon: const Icon(Icons.grid_view), label: AppLocalizations.of(context)!.navList),
BottomNavigationBarItem(icon: const Icon(Icons.games), label: AppLocalizations.of(context)!.navGuess),
BottomNavigationBarItem(icon: const Icon(Icons.settings), label: AppLocalizations.of(context)!.navSystem),
],
),
),
+48 -17
View File
@@ -1,49 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart';
import '../providers/locale_provider.dart';
import '../providers/repository_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/detail/pokemon_detail_top.dart';
import '../widgets/detail/pokemon_stats_panel.dart';
/// Fiche détaillée d'un Pokémon (reçu via les arguments de route).
class PokemonDetailPage extends StatefulWidget {
class PokemonDetailPage extends ConsumerStatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key);
@override
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
ConsumerState<PokemonDetailPage> createState() => _PokemonDetailPageState();
}
class _PokemonDetailPageState extends State<PokemonDetailPage> {
class _PokemonDetailPageState extends ConsumerState<PokemonDetailPage> {
bool _isShiny = false;
String? _englishGenus;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_fetchEnglishGenusIfNeeded();
}
Future<void> _fetchEnglishGenusIfNeeded() async {
final lang = ref.read(localeProvider).languageCode;
if (lang != 'en') return;
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
final genus = await ref.read(pokemonRepositoryProvider).getEnglishGenus(pokemon.id);
if (mounted && genus != null) setState(() => _englishGenus = genus);
}
@override
Widget build(BuildContext context) {
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
final palette = appPalettes[ref.watch(themeProvider)];
final primaryDark = HSLColor.fromColor(palette.primary)
.withLightness(
(HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0))
.toColor();
return Scaffold(
backgroundColor: const Color(0xFF1B2333),
backgroundColor: palette.surface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD32F2F),
color: palette.primary,
borderRadius: BorderRadius.circular(30),
border: Border.all(color: const Color(0xFFA12020), width: 4),
border: Border.all(color: primaryDark, width: 4),
),
child: SingleChildScrollView(
child: Column(
children: [
_backBar(context),
_backBar(context, primaryDark),
PokemonDetailTop(
pokemon: pokemon,
isShiny: _isShiny,
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
primaryColor: palette.primary,
primaryDark: primaryDark,
surfaceColor: palette.surface,
),
const SizedBox(height: 20),
_hinge(),
_hinge(primaryDark),
const SizedBox(height: 20),
PokemonStatsPanel(pokemon: pokemon),
PokemonStatsPanel(
pokemon: pokemon,
surfaceColor: palette.surface,
descriptionOverride: _englishGenus,
),
const SizedBox(height: 30),
_bottomDots(),
_bottomDots(primaryDark),
const SizedBox(height: 20),
],
),
@@ -54,7 +85,7 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
);
}
Widget _backBar(BuildContext context) {
Widget _backBar(BuildContext context, Color primaryDark) {
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -63,24 +94,24 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
);
}
Widget _hinge() {
Widget _hinge(Color primaryDark) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
Container(height: 6, width: 40, color: primaryDark),
Container(height: 6, width: 40, color: primaryDark),
],
);
}
Widget _bottomDots() {
Widget _bottomDots(Color primaryDark) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
@@ -89,7 +120,7 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
),
),
);
+18 -12
View File
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/pokemon_tile.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/list/pokedex_list_header.dart';
@@ -32,6 +34,8 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final palette = appPalettes[ref.watch(themeProvider)];
final pokedexAsync = ref.watch(pokedexProvider);
final caughtCount = ref.watch(caughtCountProvider);
@@ -45,8 +49,8 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
height: 40,
child: Row(
children: [
_buildTab('ALL', _filter == 'ALL'),
_buildTab('CAUGHT', _filter == 'CAUGHT'),
_buildTab('ALL', l.tabAll, _filter == 'ALL', palette.primary),
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT', palette.primary),
],
),
),
@@ -58,7 +62,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Text('Erreur de chargement\n$e',
child: Text('${l.loadingError}\n$e',
textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
),
data: (all) {
@@ -77,10 +81,10 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
),
Container(
height: 24,
color: const Color(0xFF1B2333),
color: palette.surface,
alignment: Alignment.center,
child: const Text('NATIONAL POKEDEX V2.0',
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
child: Text(l.pokedexFooter,
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
),
],
),
@@ -88,25 +92,27 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
}
Widget _emptyState() {
final l = AppLocalizations.of(context)!;
final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16),
Text('NO POKEMON FOUND IN $_filter',
Text(l.noPokemonFound(filterLabel),
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildTab(String title, bool isSelected) {
Widget _buildTab(String key, String label, bool isSelected, Color primaryColor) {
return Expanded(
child: GestureDetector(
onTap: () {
if (_filter != title) {
setState(() => _filter = title);
if (_filter != key) {
setState(() => _filter = key);
if (_scrollController.hasClients) _scrollController.jumpTo(0);
}
},
@@ -114,11 +120,11 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
border: isSelected
? const Border(bottom: BorderSide(color: Color(0xFFD32F2F), width: 3))
? Border(bottom: BorderSide(color: primaryColor, width: 3))
: null,
),
alignment: Alignment.center,
child: Text(title,
child: Text(label,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
),
+21 -9
View File
@@ -2,14 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart';
import '../providers/locale_provider.dart';
import '../widgets/system/system_header.dart';
import '../widgets/system/section_title.dart';
import '../widgets/system/system_stats.dart';
import '../widgets/system/palette_picker.dart';
import '../widgets/system/language_picker.dart';
/// Page "Système" : statistiques de jeu et sélection de la palette de couleurs.
/// Page "Système" : statistiques de jeu, langue et palette de couleurs.
class SystemPage extends ConsumerWidget {
const SystemPage({super.key});
@@ -20,6 +23,7 @@ class SystemPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context)!;
final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex];
final pokedexAsync = ref.watch(pokedexProvider);
@@ -28,18 +32,18 @@ class SystemPage extends ConsumerWidget {
color: const Color(0xFFC8D1D8),
child: Column(
children: [
SystemHeader(primaryColor: palette.primary),
SystemHeader(primaryColor: palette.primary, title: l.system),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionTitle(text: 'STATISTIQUES', color: palette.primary),
SectionTitle(text: l.statistics, color: palette.primary),
const SizedBox(height: 8),
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Text('Erreur de chargement'),
error: (_, __) => Text(l.loadingError),
data: (pokemons) {
final total = pokemons.length;
final caught = pokemons.where((p) => p.isCaught).length;
@@ -52,10 +56,10 @@ class SystemPage extends ConsumerWidget {
return StatsGrid(
primaryColor: palette.primary,
items: [
StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events),
StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon),
StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility),
StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart),
],
);
},
@@ -63,7 +67,15 @@ class SystemPage extends ConsumerWidget {
},
),
const SizedBox(height: 24),
SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
SectionTitle(text: l.language, color: palette.primary),
const SizedBox(height: 8),
LanguagePicker(
selected: ref.watch(localeProvider),
primaryColor: palette.primary,
onSelect: (loc) => ref.read(localeProvider.notifier).setLocale(loc),
),
const SizedBox(height: 24),
SectionTitle(text: l.colorPalette, color: palette.primary),
const SizedBox(height: 8),
PalettePicker(
selectedIndex: paletteIndex,
+26 -2
View File
@@ -7,6 +7,7 @@ import '../../domain/game/game_engine.dart';
import '../../domain/game/game_state.dart';
import '../../core/logger.dart';
import 'gen_filter_provider.dart';
import 'locale_provider.dart';
import 'pokedex_provider.dart';
import 'repository_provider.dart';
@@ -17,7 +18,28 @@ class GameNotifier extends Notifier<GameState> {
final _random = Random();
@override
GameState build() => const GameState();
GameState build() {
// Re-fetch the current Pokémon whenever the locale changes so the name
// (used for hints and the guess comparison) switches language instantly.
ref.listen(localeProvider, (_, __) => _reloadCurrentPokemon());
return const GameState();
}
Future<void> _reloadCurrentPokemon() async {
final current = state.currentPokemon;
if (current == null || state.status == GameStatus.loading) return;
try {
final updated = await ref.read(pokemonRepositoryProvider).getById(current.id);
if (updated != null) {
state = state.copyWith(
currentPokemon: updated.copyWith(
isCaught: current.isCaught,
isSeen: current.isSeen,
),
);
}
} catch (_) {}
}
Future<void> startNewGame() async {
state = _engine.newGame(state);
@@ -35,7 +57,9 @@ class GameNotifier extends Notifier<GameState> {
final repo = ref.read(pokemonRepositoryProvider);
final isShiny = _random.nextInt(AppConstants.shinyOdds) == 0;
final id = ref.read(genFilterProvider.notifier).randomId(_random);
final genFilter = ref.read(genFilterProvider.notifier);
await genFilter.ready;
final id = genFilter.randomId(_random);
try {
final pokemon = await repo.getById(id);
if (pokemon == null) {
@@ -6,10 +6,12 @@ import '../../core/config/app_constants.dart';
/// Gère les générations activées pour le tirage et fournit un id aléatoire dans celles-ci.
/// La sélection est persistée dans les préférences.
class GenFilterNotifier extends Notifier<Set<int>> {
late final Future<void> _ready;
@override
Set<int> build() {
_load();
// Default: all gens enabled
_ready = _load();
// Default: all gens enabled — overwritten by _load() once prefs are read.
return Set.from(List.generate(AppConstants.genRanges.length, (i) => i));
}
@@ -21,6 +23,9 @@ class GenFilterNotifier extends Notifier<Set<int>> {
}
}
/// Resolves once the persisted filter has been loaded from SharedPreferences.
Future<void> get ready => _ready;
Future<void> toggle(int genIndex) async {
final next = Set<int>.from(state);
if (next.contains(genIndex)) {
@@ -0,0 +1,33 @@
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
/// Langues prises en charge par l'application.
const List<Locale> supportedLocales = [Locale('en'), Locale('fr')];
/// Gère la langue sélectionnée, persistée dans les préférences.
class LocaleNotifier extends Notifier<Locale> {
@override
Locale build() {
_loadSaved();
return const Locale('fr');
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString(AppConstants.prefsLocale);
if (code != null && supportedLocales.any((l) => l.languageCode == code)) {
state = Locale(code);
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(AppConstants.prefsLocale, locale.languageCode);
}
}
/// Langue active de l'application.
final localeProvider = NotifierProvider<LocaleNotifier, Locale>(LocaleNotifier.new);
@@ -5,7 +5,7 @@ import 'repository_provider.dart';
/// Liste complète du Pokédex (triée par id), avec synchro initiale gérée par le repository.
class PokedexNotifier extends AsyncNotifier<List<Pokemon>> {
Future<List<Pokemon>> _load() async {
final repo = ref.read(pokemonRepositoryProvider);
final repo = ref.watch(pokemonRepositoryProvider);
final list = await repo.getAll();
list.sort((a, b) => a.id.compareTo(b.id));
return list;
@@ -4,10 +4,14 @@ import '../../data/datasources/pokemon_local_datasource.dart';
import '../../data/datasources/pokemon_remote_datasource.dart';
import '../../data/repositories/pokemon_repository_impl.dart';
import '../../domain/repositories/pokemon_repository.dart';
import 'locale_provider.dart';
/// Point d'injection unique du repository. Sur le web, pas de SQLite (local = null).
/// Point d'injection unique du repository.
/// Se reconstruit automatiquement quand la langue change,
/// ce qui invalide [pokedexProvider] en cascade.
final pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
final remote = PokemonRemoteDataSource();
final local = kIsWeb ? null : PokemonLocalDataSource();
final lang = ref.watch(localeProvider).languageCode;
final remote = PokemonRemoteDataSource(languageCode: lang);
final local = kIsWeb ? null : PokemonLocalDataSource(languageCode: lang);
return PokemonRepositoryImpl(remote: remote, local: local);
});
+8 -12
View File
@@ -2,25 +2,21 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Palette de couleurs nommée de l'application (couleur primaire + surface de fond).
/// Palette de couleurs de l'application (couleur primaire + surface de fond).
/// Le nom affiché est géré via AppLocalizations (palette0…palette4).
class AppPalette {
final String name;
final Color primary;
final Color surface;
const AppPalette({
required this.name,
required this.primary,
required this.surface,
});
const AppPalette({required this.primary, required this.surface});
}
const List<AppPalette> appPalettes = [
AppPalette(name: 'Pokédex Rouge', primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
AppPalette(name: 'Océan Bleu', primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
AppPalette(name: 'Forêt Verte', primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
AppPalette(name: 'Foudre Jaune', primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
AppPalette(name: 'Ombre Violette',primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
AppPalette(primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
AppPalette(primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
AppPalette(primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
AppPalette(primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
AppPalette(primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
];
const String _prefsPaletteIndex = 'palette_index';
+33 -4
View File
@@ -28,8 +28,37 @@ Color typeToColor(PokemonType type) {
return map[type] ?? Colors.transparent;
}
/// Nom du type avec une majuscule initiale.
String formatedTypeName(PokemonType type) {
final typeName = type.name;
return typeName[0].toUpperCase() + typeName.substring(1);
/// Nom du type localisé (EN par défaut, FR si languageCode == 'fr').
String localizedTypeName(PokemonType type, String languageCode) {
if (languageCode == 'fr') {
const fr = {
PokemonType.normal: 'Normal',
PokemonType.fighting: 'Combat',
PokemonType.flying: 'Vol',
PokemonType.poison: 'Poison',
PokemonType.ground: 'Sol',
PokemonType.rock: 'Roche',
PokemonType.bug: 'Insecte',
PokemonType.ghost: 'Spectre',
PokemonType.steel: 'Acier',
PokemonType.fire: 'Feu',
PokemonType.water: 'Eau',
PokemonType.grass: 'Plante',
PokemonType.electric: 'Électrik',
PokemonType.psychic: 'Psy',
PokemonType.ice: 'Glace',
PokemonType.dragon: 'Dragon',
PokemonType.dark: 'Ténèbres',
PokemonType.fairy: 'Fée',
PokemonType.unknown: '?',
PokemonType.shadow: '?',
};
return fr[type] ?? '?';
}
// English: capitalise the enum name
final name = type.name;
return name[0].toUpperCase() + name.substring(1);
}
/// Nom du type avec une majuscule initiale (anglais, conservé pour compatibilité).
String formatedTypeName(PokemonType type) => localizedTypeName(type, 'en');
@@ -8,12 +8,18 @@ class PokemonDetailTop extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final VoidCallback onToggleShiny;
final Color primaryColor;
final Color primaryDark;
final Color surfaceColor;
const PokemonDetailTop({
super.key,
required this.pokemon,
required this.isShiny,
required this.onToggleShiny,
required this.primaryColor,
required this.primaryDark,
required this.surfaceColor,
});
@override
@@ -21,9 +27,9 @@ class PokemonDetailTop extends StatelessWidget {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
color: surfaceColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
border: Border.all(color: surfaceColor, width: 8),
),
child: Container(
color: const Color(0xFF90A4AE),
@@ -32,7 +38,7 @@ class PokemonDetailTop extends StatelessWidget {
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: const Color(0xFF1B2333),
color: surfaceColor,
child: Text(
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
@@ -60,7 +66,11 @@ class PokemonDetailTop extends StatelessWidget {
Expanded(
child: Text(
pokemon.formatedName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: 2),
overflow: TextOverflow.ellipsis,
),
),
@@ -1,17 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../domain/entities/pokemon.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/locale_provider.dart';
/// Écran inférieur du détail : stats de base, description et éléments décoratifs.
class PokemonStatsPanel extends StatelessWidget {
/// Écran inférieur du détail : stats de base, description localisée et éléments décoratifs.
class PokemonStatsPanel extends ConsumerWidget {
final Pokemon pokemon;
const PokemonStatsPanel({super.key, required this.pokemon});
final Color surfaceColor;
final String? descriptionOverride;
const PokemonStatsPanel({
super.key,
required this.pokemon,
required this.surfaceColor,
this.descriptionOverride,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context)!;
ref.watch(localeProvider); // watch locale so widget rebuilds on language change
final description = descriptionOverride ?? pokemon.description;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: const Color(0xFF1B2333), borderRadius: BorderRadius.circular(8)),
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(8),
),
child: Container(
color: const Color(0xFFC8D1D8),
padding: const EdgeInsets.all(16),
@@ -21,12 +39,20 @@ class PokemonStatsPanel extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("BASE STATS", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Text("MODEL: DS-01", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold)),
Text(l.baseStats,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 2)),
Text("MODEL: DS-01",
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
fontWeight: FontWeight.bold)),
],
),
const Divider(color: Colors.black38, thickness: 2, height: 20),
_StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)),
_StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)),
_StatBar(label: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)),
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
@@ -34,16 +60,19 @@ class PokemonStatsPanel extends StatelessWidget {
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0xFFE2EBF0), border: Border.all(color: Colors.grey[400]!)),
decoration: BoxDecoration(
color: const Color(0xFFE2EBF0),
border: Border.all(color: Colors.grey[400]!),
),
child: Text(
pokemon.description != null && pokemon.description!.isNotEmpty
? '"${pokemon.description!}"'
: '"No description available for this Pokémon."',
description != null && description.isNotEmpty
? '"$description"'
: '"${l.noDescription}"',
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 16),
const _DecorativeLights(),
_DecorativeLights(accentColor: surfaceColor),
],
),
),
@@ -51,7 +80,6 @@ class PokemonStatsPanel extends StatelessWidget {
}
}
/// Barre d'une statistique (libellé, jauge proportionnelle, valeur).
class _StatBar extends StatelessWidget {
final String label;
final int value;
@@ -60,20 +88,27 @@ class _StatBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ratio = (value / 255).clamp(0.0, 1.0); // stat de base max supposée = 255
final ratio = (value / 255).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SizedBox(width: 50, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
SizedBox(
width: 50,
child: Text(label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(color: Colors.grey[400]),
child: Row(
children: [
Expanded(flex: (ratio * 100).toInt(), child: Container(color: color)),
Expanded(flex: 100 - (ratio * 100).toInt(), child: Container()),
Expanded(
flex: (ratio * 100).toInt(),
child: Container(color: color)),
Expanded(
flex: 100 - (ratio * 100).toInt(),
child: Container()),
],
),
),
@@ -82,7 +117,8 @@ class _StatBar extends StatelessWidget {
SizedBox(
width: 40,
child: Text(value.toString(),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), textAlign: TextAlign.right),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
textAlign: TextAlign.right),
),
],
),
@@ -90,21 +126,26 @@ class _StatBar extends StatelessWidget {
}
}
/// Petites diodes décoratives en bas du panneau (purement cosmétiques).
class _DecorativeLights extends StatelessWidget {
const _DecorativeLights();
final Color accentColor;
const _DecorativeLights({required this.accentColor});
@override
Widget build(BuildContext context) {
final dark = HSLColor.fromColor(accentColor)
.withLightness(
(HSLColor.fromColor(accentColor).lightness - 0.1).clamp(0.0, 1.0))
.toColor();
return Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: const Color(0xFF1E88E5),
color: accentColor,
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFF1565C0), width: 2),
border: Border.all(color: dark, width: 2),
),
),
const SizedBox(width: 8),
@@ -120,9 +161,19 @@ class _DecorativeLights extends StatelessWidget {
const Spacer(),
Row(
children: [
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
Container(
height: 6,
width: 30,
decoration: BoxDecoration(
color: Colors.grey[500],
borderRadius: BorderRadius.circular(3))),
const SizedBox(width: 4),
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
Container(
height: 6,
width: 30,
decoration: BoxDecoration(
color: Colors.grey[500],
borderRadius: BorderRadius.circular(3))),
],
),
],
@@ -1,19 +1,29 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
class GameOverActions extends StatelessWidget {
final Color primaryColor;
final Color primaryDark;
final VoidCallback onTryAgain;
final VoidCallback onBack;
const GameOverActions({super.key, required this.onTryAgain, required this.onBack});
const GameOverActions({
super.key,
required this.primaryColor,
required this.primaryDark,
required this.onTryAgain,
required this.onBack,
});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Column(
children: [
_button(label: "TRY AGAIN", icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain),
_button(label: l.tryAgain, icon: Icons.refresh, color: primaryColor, onPressed: onTryAgain),
const SizedBox(height: 16),
_button(label: "BACK TO POKEDEX", icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack),
_button(label: l.backToPokedex, icon: Icons.menu_book, color: primaryDark, onPressed: onBack),
],
);
}
@@ -1,20 +1,29 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart';
/// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon.
class GameOverHeader extends StatelessWidget {
final String pokemonImage;
final String pokemonName;
final Color primaryDark;
final Color surfaceColor;
const GameOverHeader({super.key, required this.pokemonImage, required this.pokemonName});
const GameOverHeader({
super.key,
required this.pokemonImage,
required this.pokemonName,
required this.primaryDark,
required this.surfaceColor,
});
static const _darkRed = Color(0xFF9E1B1B);
static const _silverBg = Color(0xFFC8D1D8);
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container(
decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)),
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
@@ -22,11 +31,11 @@ class GameOverHeader extends StatelessWidget {
child: Column(
children: [
Container(
color: _darkRed,
color: primaryDark,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: const Text(
"GAME OVER",
style: TextStyle(
child: Text(
l.gameOver,
style: const TextStyle(
fontSize: 26,
color: Colors.yellow,
fontWeight: FontWeight.bold,
@@ -40,12 +49,12 @@ class GameOverHeader extends StatelessWidget {
SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)),
const SizedBox(height: 12),
Text(
"It was $pokemonName!",
l.itWas(pokemonName),
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1B2333),
color: surfaceColor,
letterSpacing: 1,
),
),
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE).
class GameOverStats extends StatelessWidget {
@@ -10,13 +11,14 @@ class GameOverStats extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Row(
children: [
Expanded(child: _StatBox(label: "STREAK", value: streak)),
Expanded(child: _StatBox(label: l.statStreak, value: streak)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SEEN", value: seen)),
Expanded(child: _StatBox(label: l.statSeen, value: seen)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SCORE", value: score)),
Expanded(child: _StatBox(label: l.statScore, value: score)),
],
);
}
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
class HingeDivider extends StatelessWidget {
const HingeDivider({super.key});
static const _darkRed = Color(0xFF9E1B1B);
final Color color;
const HingeDivider({super.key, required this.color});
@override
Widget build(BuildContext context) {
@@ -12,7 +11,7 @@ class HingeDivider extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Row(
children: [
Expanded(child: Container(height: 2, color: _darkRed)),
Expanded(child: Container(height: 2, color: color)),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
@@ -22,12 +21,12 @@ class HingeDivider extends StatelessWidget {
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(color: _darkRed, shape: BoxShape.circle),
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
),
),
const SizedBox(width: 8),
Expanded(child: Container(height: 2, color: _darkRed)),
Expanded(child: Container(height: 2, color: color)),
],
),
);
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.dart';
import '../../../l10n/app_localizations.dart';
/// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations.
class GenFilterSection extends StatelessWidget {
@@ -7,6 +8,7 @@ class GenFilterSection extends StatelessWidget {
final VoidCallback onToggleOpen;
final Set<int> selectedGens;
final void Function(int) onToggle;
final Color surfaceColor;
const GenFilterSection({
super.key,
@@ -14,6 +16,7 @@ class GenFilterSection extends StatelessWidget {
required this.onToggleOpen,
required this.selectedGens,
required this.onToggle,
required this.surfaceColor,
});
@override
@@ -28,19 +31,19 @@ class GenFilterSection extends StatelessWidget {
onPressed: onToggleOpen,
icon: Icon(
isOpen ? Icons.expand_less : Icons.filter_list,
color: const Color(0xFF1B2333),
color: surfaceColor,
),
label: const Text(
'GEN FILTER',
label: Text(
AppLocalizations.of(context)!.genFilter,
style: TextStyle(
color: Color(0xFF1B2333),
color: surfaceColor,
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 2,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
side: BorderSide(color: surfaceColor, width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
@@ -50,7 +53,7 @@ class GenFilterSection extends StatelessWidget {
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: isOpen
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle)
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle, surfaceColor: surfaceColor)
: const SizedBox.shrink(),
),
],
@@ -62,8 +65,9 @@ class GenFilterSection extends StatelessWidget {
class _GenFilterPanel extends StatelessWidget {
final Set<int> selectedGens;
final void Function(int) onToggle;
final Color surfaceColor;
const _GenFilterPanel({required this.selectedGens, required this.onToggle});
const _GenFilterPanel({required this.selectedGens, required this.onToggle, required this.surfaceColor});
static const _genNames = [
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
@@ -75,7 +79,7 @@ class _GenFilterPanel extends StatelessWidget {
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFF1B2333), width: 2),
border: Border.all(color: surfaceColor, width: 2),
),
child: Column(
children: List.generate(AppConstants.genRanges.length, (i) {
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Section de saisie : indice optionnel, champ de réponse et boutons d'action
/// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks.
@@ -36,14 +37,15 @@ class GuessInputSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
Text(
l.identificationInput,
style: const TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
if (isHintUsed)
@@ -57,7 +59,7 @@ class GuessInputSection extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
),
child: Text(
"HINT: ${_maskedName(pokemonName)}",
"${l.hintPrefix}: ${_maskedName(pokemonName)}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
),
@@ -67,28 +69,28 @@ class GuessInputSection extends StatelessWidget {
child: TextField(
controller: controller,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: 'Enter Pokémon name...',
hintText: l.enterPokemonName,
),
onSubmitted: (_) => onGuess(),
),
),
const SizedBox(height: 16),
if (isGuessed)
_bigButton(label: "CONTINUE", color: Colors.green, onPressed: onContinue)
_bigButton(label: l.continueButton, color: Colors.green, onPressed: onContinue)
else ...[
_bigButton(label: "GUESS!", color: const Color(0xFF3B6EE3), onPressed: onGuess),
_bigButton(label: l.guessButton, color: const Color(0xFF3B6EE3), onPressed: onGuess),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _actionButton(icon: Icons.lightbulb, label: "HINT ($hints)", color: Colors.amber, onPressed: onHint),
child: _actionButton(icon: Icons.lightbulb, label: l.hintButton(hints), color: Colors.amber, onPressed: onHint),
),
const SizedBox(width: 8),
Expanded(
child: _actionButton(icon: Icons.skip_next, label: "SKIP ($skips)", color: Colors.grey[400]!, onPressed: onSkip),
child: _actionButton(icon: Icons.skip_next, label: l.skipButton(skips), color: Colors.grey[400]!, onPressed: onSkip),
),
],
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart';
/// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée).
@@ -7,16 +8,19 @@ class GuessSilhouette extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final bool isGuessed;
final Color surfaceColor;
const GuessSilhouette({
super.key,
required this.pokemon,
required this.isShiny,
required this.isGuessed,
required this.surfaceColor,
});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl;
return Container(
height: 250,
@@ -25,7 +29,7 @@ class GuessSilhouette extends StatelessWidget {
decoration: BoxDecoration(
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
border: Border.all(color: surfaceColor, width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -49,11 +53,11 @@ class GuessSilhouette extends StatelessWidget {
),
),
Container(
color: const Color(0xFF1B2333),
color: surfaceColor,
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
isShiny ? l.shinyDetected : l.whosThatPokemon,
textAlign: TextAlign.center,
style: TextStyle(
color: isShiny ? Colors.yellow[400] : Colors.white,
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Encart affichant le score courant et le meilleur score personnel.
class ScoreBoard extends StatelessWidget {
@@ -9,6 +10,7 @@ class ScoreBoard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
@@ -19,9 +21,9 @@ class ScoreBoard extends StatelessWidget {
),
child: Column(
children: [
const Text(
"CURRENT SCORE",
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
Text(
l.currentScore,
style: const TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
"$currentScore",
@@ -29,7 +31,7 @@ class ScoreBoard extends StatelessWidget {
),
const Divider(height: 24),
Text(
"PERSONAL BEST: $bestScore",
l.personalBest(bestScore),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Bandeau affichant le nombre de Pokémon découverts sur le total.
class PokedexCountBar extends StatelessWidget {
@@ -20,8 +21,8 @@ class PokedexCountBar extends StatelessWidget {
'${caught.toString().padLeft(3, '0')} / $total',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const Text('POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
Text(AppLocalizations.of(context)!.pokemonDiscovered,
style: const TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
],
),
);
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Barre de titre de la liste du Pokédex.
class PokedexListHeader extends StatelessWidget {
@@ -9,15 +10,9 @@ class PokedexListHeader extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.menu, color: Colors.black87),
Text('LIST - NATIONAL',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Icon(Icons.search, color: Colors.black87),
],
),
alignment: Alignment.center,
child: Text(AppLocalizations.of(context)!.listNational,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
);
}
}
+2 -1
View File
@@ -10,7 +10,8 @@ class PokemonTypeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
String typeName = formatedTypeName(type);
final lang = Localizations.localeOf(context).languageCode;
String typeName = localizedTypeName(type, lang);
Color typeColor = typeToColor(type);
return Container(
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/locale_provider.dart';
/// Sélecteur de langue de l'application (parmi [supportedLocales]).
class LanguagePicker extends StatelessWidget {
final Locale selected;
final Color primaryColor;
final void Function(Locale) onSelect;
const LanguagePicker({
super.key,
required this.selected,
required this.primaryColor,
required this.onSelect,
});
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final names = {'en': l.langEnglish, 'fr': l.langFrench};
return Column(
children: supportedLocales.map((locale) {
final isSelected = locale.languageCode == selected.languageCode;
return GestureDetector(
onTap: () => onSelect(locale),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? primaryColor : Colors.grey.shade300,
width: isSelected ? 3 : 1.5,
),
),
child: Row(
children: [
Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)),
const SizedBox(width: 16),
Text(
names[locale.languageCode] ?? locale.languageCode,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.black87,
),
),
const Spacer(),
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
],
),
),
);
}).toList(),
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/theme_provider.dart';
/// Sélecteur de palette de couleurs de l'application.
@@ -9,6 +10,9 @@ class PalettePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final paletteNames = [l.palette0, l.palette1, l.palette2, l.palette3, l.palette4];
return Column(
children: List.generate(appPalettes.length, (i) {
final p = appPalettes[i];
@@ -53,7 +57,7 @@ class PalettePicker extends StatelessWidget {
),
const SizedBox(width: 16),
Text(
p.name,
paletteNames[i],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
/// En-tête de la page Système.
class SystemHeader extends StatelessWidget {
final Color primaryColor;
const SystemHeader({super.key, required this.primaryColor});
final String title;
const SystemHeader({super.key, required this.primaryColor, required this.title});
@override
Widget build(BuildContext context) {
@@ -14,10 +15,10 @@ class SystemHeader extends StatelessWidget {
color: primaryColor,
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
),
child: const Text(
'SYSTÈME',
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
),
);
}
+1
View File
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
+6
View File
@@ -11,6 +11,11 @@ dependencies:
flutter:
sdk: flutter
# Internationalisation (FR / EN)
flutter_localizations:
sdk: flutter
intl: any
# Shared API
sqflite_common: ^2.5.0
@@ -35,3 +40,4 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true # active la génération des localisations (gen-l10n)
+1
View File
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)