Compare commits
6
Commits
9a41bd50a6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8608fc023c | ||
|
|
8f93a18a70 | ||
|
|
3d2f5be5e5 | ||
|
|
3984e94bc5 | ||
|
|
47536843be | ||
|
|
0952b372b4 |
@@ -80,8 +80,3 @@ repository.
|
|||||||
- shiny : `.../sprites/<id>/shiny.png`
|
- shiny : `.../sprites/<id>/shiny.png`
|
||||||
- [Cris des Pokémon](https://pokemoncries.com/cries/1.mp3)
|
- [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
@@ -8,23 +8,74 @@ par Riverpod (providers manuels).
|
|||||||
|
|
||||||
## Couches
|
## 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)
|
### domain (Dart pur)
|
||||||
|
|
||||||
- **Entités** : `Pokemon`, immuable, sans dépendance Flutter/DB/API.
|
- **Entités** : `Pokemon`, immuable, sans dépendance Flutter/DB/API.
|
||||||
- **Repository (interface)** : `PokemonRepository` définit le contrat d'accès aux données.
|
- **Repository (interface)** : `PokemonRepository` définit le contrat d'accès aux données.
|
||||||
- **Jeu** : `GameState` (état immuable) et `GameEngine` (règles pures, testables).
|
- **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
|
### data
|
||||||
|
|
||||||
- **DTO** : `PokemonDto` centralise tout le parsing JSON (API Tyradex + SQLite).
|
- **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 ».
|
- **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
|
### presentation
|
||||||
- **Providers** : `pokemonRepositoryProvider` (DI), `pokedexProvider` (`AsyncNotifier`),
|
|
||||||
`gameProvider` (`Notifier<GameState>`), `selectedTabProvider` (onglet courant).
|
#### Providers (Riverpod)
|
||||||
- **Pages** : `ConsumerWidget` / `ConsumerStatefulWidget` qui observent les providers.
|
|
||||||
- **Widgets** : éléments réutilisables (`PokemonImage`, `PokemonTile`, `PokemonTypeWidget`).
|
| Provider | Type | Rôle |
|
||||||
- **Thème** : `type_colors.dart` (couleur/format des types).
|
| --- | --- | --- |
|
||||||
|
| `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
|
## 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
|
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.
|
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 I–IX ; persisté entre les sessions.
|
||||||
|
- **Acceptation bilingue** : le nom FR *ou* EN est accepté quelle que soit la langue de l'UI.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
- `test/domain/game_engine_test.dart` : règles du jeu.
|
- `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/data/pokemon_repository_test.dart` : logique du repository (datasources factices).
|
||||||
|
- `test/widget_test.dart` : smoke test de démarrage de l'app.
|
||||||
|
|||||||
+26
-11
@@ -2,15 +2,27 @@
|
|||||||
|
|
||||||
## Description
|
## 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
|
## Features
|
||||||
|
|
||||||
- National Pokedex: Browse all 1025+ Pokemon from all generations.
|
- **National Pokédex**: Browse all 1025 Pokémon from generations I to IX.
|
||||||
- Guess Game: Identify Pokemon by their silhouette.
|
- **Guess Game**: Identify Pokémon by their silhouette.
|
||||||
- Scoring System: Earn points for correct guesses, with bonuses for Shiny Pokemon. High scores are saved locally.
|
- **Scoring System**: Earn 10 pts per correct guess, 20 pts for Shiny Pokémon (1 in 10 chance).
|
||||||
- Collection: Track caught and seen Pokemon.
|
High scores are saved locally.
|
||||||
- Search and Filter: Filter the collection by all or caught status and search by name.
|
- **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 (I–IX). 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
|
## Installation
|
||||||
|
|
||||||
@@ -21,8 +33,11 @@ Pokeguess is a Flutter mobile application that allows users to discover and coll
|
|||||||
|
|
||||||
## Technologies
|
## Technologies
|
||||||
|
|
||||||
- Flutter: UI Framework.
|
- **Flutter**: UI Framework.
|
||||||
- SQLite (sqflite): Local database.
|
- **Riverpod**: State management (providers + notifiers).
|
||||||
- Tyradex API: Pokemon data source.
|
- **SQLite (sqflite)**: Local database for offline-first Pokémon storage.
|
||||||
- Shared Preferences: High score persistence.
|
- **Tyradex API**: Primary Pokémon data source (FR/EN names, sprites, types, stats).
|
||||||
- Google Fonts: Custom typography.
|
- **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.
|
||||||
|
|||||||
@@ -2,24 +2,43 @@ import 'package:sqflite_common/sqflite.dart';
|
|||||||
import '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import '../dto/pokemon_dto.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 {
|
class PokemonLocalDataSource {
|
||||||
|
final String languageCode;
|
||||||
|
|
||||||
|
PokemonLocalDataSource({this.languageCode = 'fr'});
|
||||||
|
|
||||||
Future<Database>? _dbFuture;
|
Future<Database>? _dbFuture;
|
||||||
|
|
||||||
Future<Database> _getDb() {
|
Future<Database> _getDb() {
|
||||||
return _dbFuture ??= openDatabase(
|
return _dbFuture ??= openDatabase(
|
||||||
'pokedex.db',
|
'pokedex.db',
|
||||||
version: 2,
|
version: 5,
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
if (oldVersion < 2) {
|
// Drop and recreate on any upgrade to pick up new columns cleanly.
|
||||||
await db.execute('DROP TABLE IF EXISTS pokemon');
|
await db.execute('DROP TABLE IF EXISTS pokemon');
|
||||||
await db.execute(
|
await db.execute(_createSql);
|
||||||
'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)');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onCreate: (db, version) async {
|
onCreate: (db, version) async {
|
||||||
await db.execute(
|
await db.execute(_createSql);
|
||||||
'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)');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,14 +46,14 @@ class PokemonLocalDataSource {
|
|||||||
Future<List<Pokemon>> getAll() async {
|
Future<List<Pokemon>> getAll() async {
|
||||||
final db = await _getDb();
|
final db = await _getDb();
|
||||||
final rows = await db.query('pokemon');
|
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 {
|
Future<Pokemon?> getById(int id) async {
|
||||||
final db = await _getDb();
|
final db = await _getDb();
|
||||||
final rows = await db.query('pokemon', where: 'id = ?', whereArgs: [id]);
|
final rows = await db.query('pokemon', where: 'id = ?', whereArgs: [id]);
|
||||||
if (rows.isEmpty) return null;
|
if (rows.isEmpty) return null;
|
||||||
return PokemonDto.fromDb(rows.first);
|
return PokemonDto.fromDb(rows.first, languageCode: languageCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveAll(List<Pokemon> pokemons) async {
|
Future<void> saveAll(List<Pokemon> pokemons) async {
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import '../dto/pokemon_dto.dart';
|
|||||||
/// Accès distant à l'API Tyradex.
|
/// Accès distant à l'API Tyradex.
|
||||||
class PokemonRemoteDataSource {
|
class PokemonRemoteDataSource {
|
||||||
final http.Client _client;
|
final http.Client _client;
|
||||||
|
final String languageCode;
|
||||||
|
|
||||||
PokemonRemoteDataSource({http.Client? client})
|
PokemonRemoteDataSource({http.Client? client, this.languageCode = 'fr'})
|
||||||
: _client = client ?? http.Client();
|
: _client = client ?? http.Client();
|
||||||
|
|
||||||
Future<Pokemon> getById(int id) async {
|
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
|
final response = await _client
|
||||||
.get(Uri.https(AppConstants.apiBaseUrl, '${AppConstants.apiPokemonPath}/$id'));
|
.get(Uri.https(AppConstants.apiBaseUrl, '${AppConstants.apiPokemonPath}/$id'));
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
@@ -21,11 +22,29 @@ class PokemonRemoteDataSource {
|
|||||||
'Erreur récupération du pokémon $id, code ${response.statusCode}');
|
'Erreur récupération du pokémon $id, code ${response.statusCode}');
|
||||||
}
|
}
|
||||||
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
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 {
|
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
|
final response = await _client
|
||||||
.get(Uri.https(AppConstants.apiBaseUrl, AppConstants.apiPokemonPath));
|
.get(Uri.https(AppConstants.apiBaseUrl, AppConstants.apiPokemonPath));
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
@@ -34,9 +53,12 @@ class PokemonRemoteDataSource {
|
|||||||
final List<dynamic> jsonList = jsonDecode(response.body);
|
final List<dynamic> jsonList = jsonDecode(response.body);
|
||||||
final result = <Pokemon>[];
|
final result = <Pokemon>[];
|
||||||
for (final json in jsonList) {
|
for (final json in jsonList) {
|
||||||
if (json['pokedex_id'] == 0) continue; // entrée générique Tyradex
|
if (json['pokedex_id'] == 0) continue;
|
||||||
try {
|
try {
|
||||||
result.add(PokemonDto.fromTyradexJson(json as Map<String, dynamic>));
|
result.add(PokemonDto.fromTyradexJson(
|
||||||
|
json as Map<String, dynamic>,
|
||||||
|
languageCode: languageCode,
|
||||||
|
));
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st);
|
AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import '../../domain/entities/pokemon.dart';
|
|||||||
class PokemonDto {
|
class PokemonDto {
|
||||||
PokemonDto._();
|
PokemonDto._();
|
||||||
|
|
||||||
/// Mappe un nom de type français (API Tyradex) vers l'enum.
|
static PokemonType _frenchTypeToEnum(String type) {
|
||||||
static PokemonType frenchTypeToEnum(String frenchType) {
|
|
||||||
const map = {
|
const map = {
|
||||||
'Normal': PokemonType.normal,
|
'Normal': PokemonType.normal,
|
||||||
'Combat': PokemonType.fighting,
|
'Combat': PokemonType.fighting,
|
||||||
@@ -26,41 +25,65 @@ class PokemonDto {
|
|||||||
'Ténèbres': PokemonType.dark,
|
'Ténèbres': PokemonType.dark,
|
||||||
'Fée': PokemonType.fairy,
|
'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).
|
// Tyradex always returns type names in French regardless of endpoint language.
|
||||||
/// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`.
|
static PokemonType _typeToEnum(String typeName, String languageCode) {
|
||||||
static Pokemon fromTyradexJson(Map<String, dynamic> json, {int? fallbackId}) {
|
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?) ??
|
final id = (json['pokedex_id'] as int?) ??
|
||||||
fallbackId ??
|
fallbackId ??
|
||||||
(throw ArgumentError('pokedex_id absent et fallbackId non fourni'));
|
(throw ArgumentError('pokedex_id absent et fallbackId non fourni'));
|
||||||
|
|
||||||
final nameMap = json['name'] as Map<String, dynamic>?;
|
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 List types = json['types'] ?? [];
|
||||||
final type1 =
|
// Tyradex retourne toujours les types dans la langue du endpoint (fr ou en).
|
||||||
types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown;
|
final type1 = types.isNotEmpty
|
||||||
final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null;
|
? _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'];
|
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(
|
return Pokemon(
|
||||||
name: name,
|
name: name,
|
||||||
|
nameFr: nameFr,
|
||||||
|
nameEn: nameEn,
|
||||||
id: id,
|
id: id,
|
||||||
type1: type1,
|
type1: type1,
|
||||||
type2: type2,
|
type2: type2,
|
||||||
hp: stats?['hp'] ?? 0,
|
hp: stats?['hp'] ?? 0,
|
||||||
atk: stats?['atk'] ?? 0,
|
atk: stats?['atk'] ?? 0,
|
||||||
def: stats?['def'] ?? 0,
|
def: stats?['def'] ?? 0,
|
||||||
spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex
|
spd: stats?['vit'] ?? 0,
|
||||||
description: json['category'],
|
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) {
|
static Map<String, dynamic> toDb(Pokemon p) {
|
||||||
return {
|
return {
|
||||||
'name': p.name,
|
'name': p.name,
|
||||||
|
'name_fr': p.nameFr ?? p.name,
|
||||||
|
'name_en': p.nameEn ?? p.name,
|
||||||
'id': p.id,
|
'id': p.id,
|
||||||
'type1': p.type1.name,
|
'type1': p.type1.name,
|
||||||
'type2': p.type2?.name,
|
'type2': p.type2?.name,
|
||||||
@@ -69,15 +92,23 @@ class PokemonDto {
|
|||||||
'def': p.def,
|
'def': p.def,
|
||||||
'spd': p.spd,
|
'spd': p.spd,
|
||||||
'description': p.description,
|
'description': p.description,
|
||||||
|
'description_en': p.descriptionEn,
|
||||||
'isCaught': p.isCaught ? 1 : 0,
|
'isCaught': p.isCaught ? 1 : 0,
|
||||||
'isSeen': p.isSeen ? 1 : 0,
|
'isSeen': p.isSeen ? 1 : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconstruit depuis une ligne SQLite.
|
/// 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(
|
return Pokemon(
|
||||||
name: row['name'],
|
name: name,
|
||||||
|
nameFr: nameFr,
|
||||||
|
nameEn: nameEn,
|
||||||
id: row['id'],
|
id: row['id'],
|
||||||
type1: PokemonType.values.firstWhere(
|
type1: PokemonType.values.firstWhere(
|
||||||
(e) => e.name == row['type1'],
|
(e) => e.name == row['type1'],
|
||||||
@@ -94,6 +125,7 @@ class PokemonDto {
|
|||||||
def: row['def'] ?? 0,
|
def: row['def'] ?? 0,
|
||||||
spd: row['spd'] ?? 0,
|
spd: row['spd'] ?? 0,
|
||||||
description: row['description'],
|
description: row['description'],
|
||||||
|
descriptionEn: row['description_en'] as String?,
|
||||||
isCaught: row['isCaught'] == 1 || row['isCaught'] == true,
|
isCaught: row['isCaught'] == 1 || row['isCaught'] == true,
|
||||||
isSeen: row['isSeen'] == 1 || row['isSeen'] == true,
|
isSeen: row['isSeen'] == 1 || row['isSeen'] == true,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,4 +60,7 @@ class PokemonRepositoryImpl implements PokemonRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> seenCount() async => (await local?.seenCount()) ?? 0;
|
Future<int> seenCount() async => (await local?.seenCount()) ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getEnglishGenus(int id) => remote.getEnglishGenus(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API.
|
/// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API.
|
||||||
class Pokemon {
|
class Pokemon {
|
||||||
final String name;
|
final String name;
|
||||||
|
final String? nameFr;
|
||||||
|
final String? nameEn;
|
||||||
final int id;
|
final int id;
|
||||||
final PokemonType type1;
|
final PokemonType type1;
|
||||||
final PokemonType? type2;
|
final PokemonType? type2;
|
||||||
@@ -9,11 +11,14 @@ class Pokemon {
|
|||||||
final int def;
|
final int def;
|
||||||
final int spd;
|
final int spd;
|
||||||
final String? description;
|
final String? description;
|
||||||
|
final String? descriptionEn;
|
||||||
final bool isCaught;
|
final bool isCaught;
|
||||||
final bool isSeen;
|
final bool isSeen;
|
||||||
|
|
||||||
const Pokemon({
|
const Pokemon({
|
||||||
required this.name,
|
required this.name,
|
||||||
|
this.nameFr,
|
||||||
|
this.nameEn,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.type1,
|
required this.type1,
|
||||||
this.type2,
|
this.type2,
|
||||||
@@ -22,6 +27,7 @@ class Pokemon {
|
|||||||
required this.def,
|
required this.def,
|
||||||
required this.spd,
|
required this.spd,
|
||||||
this.description,
|
this.description,
|
||||||
|
this.descriptionEn,
|
||||||
this.isCaught = false,
|
this.isCaught = false,
|
||||||
this.isSeen = false,
|
this.isSeen = false,
|
||||||
});
|
});
|
||||||
@@ -37,6 +43,8 @@ class Pokemon {
|
|||||||
|
|
||||||
Pokemon copyWith({
|
Pokemon copyWith({
|
||||||
String? name,
|
String? name,
|
||||||
|
String? nameFr,
|
||||||
|
String? nameEn,
|
||||||
int? id,
|
int? id,
|
||||||
PokemonType? type1,
|
PokemonType? type1,
|
||||||
PokemonType? type2,
|
PokemonType? type2,
|
||||||
@@ -45,11 +53,14 @@ class Pokemon {
|
|||||||
int? def,
|
int? def,
|
||||||
int? spd,
|
int? spd,
|
||||||
String? description,
|
String? description,
|
||||||
|
String? descriptionEn,
|
||||||
bool? isCaught,
|
bool? isCaught,
|
||||||
bool? isSeen,
|
bool? isSeen,
|
||||||
}) {
|
}) {
|
||||||
return Pokemon(
|
return Pokemon(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
|
nameFr: nameFr ?? this.nameFr,
|
||||||
|
nameEn: nameEn ?? this.nameEn,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
type1: type1 ?? this.type1,
|
type1: type1 ?? this.type1,
|
||||||
type2: type2 ?? this.type2,
|
type2: type2 ?? this.type2,
|
||||||
@@ -58,6 +69,7 @@ class Pokemon {
|
|||||||
def: def ?? this.def,
|
def: def ?? this.def,
|
||||||
spd: spd ?? this.spd,
|
spd: spd ?? this.spd,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
|
descriptionEn: descriptionEn ?? this.descriptionEn,
|
||||||
isCaught: isCaught ?? this.isCaught,
|
isCaught: isCaught ?? this.isCaught,
|
||||||
isSeen: isSeen ?? this.isSeen,
|
isSeen: isSeen ?? this.isSeen,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ class GameEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final normalizedGuess = _normalize(guess.trim().toLowerCase());
|
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 newSession = s.sessionCorrectCount + 1;
|
||||||
final gained = s.isShiny ? AppConstants.pointsShiny : AppConstants.pointsNormal;
|
final gained = s.isShiny ? AppConstants.pointsShiny : AppConstants.pointsNormal;
|
||||||
final newScore = s.currentScore + gained;
|
final newScore = s.currentScore + gained;
|
||||||
|
|||||||
@@ -19,4 +19,7 @@ abstract interface class PokemonRepository {
|
|||||||
|
|
||||||
/// Nombre de Pokémon vus.
|
/// Nombre de Pokémon vus.
|
||||||
Future<int> seenCount();
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -53,5 +53,18 @@
|
|||||||
"statBestScore": "Best score",
|
"statBestScore": "Best score",
|
||||||
"statCaught": "Caught",
|
"statCaught": "Caught",
|
||||||
"statSeenLabel": "Seen",
|
"statSeenLabel": "Seen",
|
||||||
"statCompletion": "Completion"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -46,5 +46,18 @@
|
|||||||
"statBestScore": "Meilleur score",
|
"statBestScore": "Meilleur score",
|
||||||
"statCaught": "Attrapés",
|
"statCaught": "Attrapés",
|
||||||
"statSeenLabel": "Vus",
|
"statSeenLabel": "Vus",
|
||||||
"statCompletion": "Complétion"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,66 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Completion'**
|
/// **'Completion'**
|
||||||
String get statCompletion;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -148,4 +148,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get statCompletion => 'Completion';
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,4 +148,34 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get statCompletion => 'Complétion';
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../providers/repository_provider.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_header.dart';
|
||||||
import '../widgets/game_over/game_over_stats.dart';
|
import '../widgets/game_over/game_over_stats.dart';
|
||||||
import '../widgets/game_over/game_over_actions.dart';
|
import '../widgets/game_over/game_over_actions.dart';
|
||||||
@@ -44,12 +45,14 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
final int streak = args?['streak'] ?? 0;
|
final int streak = args?['streak'] ?? 0;
|
||||||
final int score = args?['score'] ?? 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 silverBg = Color(0xFFC8D1D8);
|
||||||
const messageBoxBg = Color(0xFF1B2333);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFD32F2F),
|
backgroundColor: palette.primary,
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
@@ -57,10 +60,15 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
GameOverHeader(pokemonImage: pokemonImage, pokemonName: pokemonName),
|
GameOverHeader(
|
||||||
const HingeDivider(),
|
pokemonImage: pokemonImage,
|
||||||
|
pokemonName: pokemonName,
|
||||||
|
primaryDark: primaryDark,
|
||||||
|
surfaceColor: palette.surface,
|
||||||
|
),
|
||||||
|
HingeDivider(color: primaryDark),
|
||||||
Container(
|
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(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -69,7 +77,7 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: messageBoxBg,
|
color: palette.surface,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.gameOverMessage,
|
AppLocalizations.of(context)!.gameOverMessage,
|
||||||
@@ -85,6 +93,8 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GameOverActions(
|
GameOverActions(
|
||||||
|
primaryColor: palette.primary,
|
||||||
|
primaryDark: primaryDark,
|
||||||
onTryAgain: () => Navigator.pop(context, true),
|
onTryAgain: () => Navigator.pop(context, true),
|
||||||
onBack: () => Navigator.pop(context, false),
|
onBack: () => Navigator.pop(context, false),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart';
|
|||||||
import '../providers/gen_filter_provider.dart';
|
import '../providers/gen_filter_provider.dart';
|
||||||
import '../providers/game_provider.dart';
|
import '../providers/game_provider.dart';
|
||||||
import '../providers/navigation_provider.dart';
|
import '../providers/navigation_provider.dart';
|
||||||
|
import '../providers/theme_provider.dart';
|
||||||
import '../widgets/scanline_overlay.dart';
|
import '../widgets/scanline_overlay.dart';
|
||||||
import '../widgets/guess/guess_silhouette.dart';
|
import '../widgets/guess/guess_silhouette.dart';
|
||||||
import '../widgets/guess/gen_filter_section.dart';
|
import '../widgets/guess/gen_filter_section.dart';
|
||||||
@@ -92,6 +93,7 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = ref.watch(gameProvider);
|
final state = ref.watch(gameProvider);
|
||||||
|
final palette = appPalettes[ref.watch(themeProvider)];
|
||||||
|
|
||||||
if (state.status == GameStatus.loading) {
|
if (state.status == GameStatus.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -112,12 +114,13 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed),
|
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed, surfaceColor: palette.surface),
|
||||||
GenFilterSection(
|
GenFilterSection(
|
||||||
isOpen: _genFilterOpen,
|
isOpen: _genFilterOpen,
|
||||||
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
|
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
|
||||||
selectedGens: ref.watch(genFilterProvider),
|
selectedGens: ref.watch(genFilterProvider),
|
||||||
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
||||||
|
surfaceColor: palette.surface,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LivesRow(lives: state.lives),
|
LivesRow(lives: state.lives),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../providers/navigation_provider.dart';
|
import '../providers/navigation_provider.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
import 'pokemon_list.dart';
|
import 'pokemon_list.dart';
|
||||||
@@ -56,10 +57,10 @@ class MainPage extends ConsumerWidget {
|
|||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
selectedItemColor: palette.primary,
|
selectedItemColor: palette.primary,
|
||||||
unselectedItemColor: Colors.grey,
|
unselectedItemColor: Colors.grey,
|
||||||
items: const [
|
items: [
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'LIST'),
|
BottomNavigationBarItem(icon: const Icon(Icons.grid_view), label: AppLocalizations.of(context)!.navList),
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.games), label: 'GUESS'),
|
BottomNavigationBarItem(icon: const Icon(Icons.games), label: AppLocalizations.of(context)!.navGuess),
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'SYSTEM'),
|
BottomNavigationBarItem(icon: const Icon(Icons.settings), label: AppLocalizations.of(context)!.navSystem),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,49 +1,80 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/pokemon.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_detail_top.dart';
|
||||||
import '../widgets/detail/pokemon_stats_panel.dart';
|
import '../widgets/detail/pokemon_stats_panel.dart';
|
||||||
|
|
||||||
/// Fiche détaillée d'un Pokémon (reçu via les arguments de route).
|
/// 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);
|
const PokemonDetailPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
|
ConsumerState<PokemonDetailPage> createState() => _PokemonDetailPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
class _PokemonDetailPageState extends ConsumerState<PokemonDetailPage> {
|
||||||
bool _isShiny = false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
|
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(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF1B2333),
|
backgroundColor: palette.surface,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFD32F2F),
|
color: palette.primary,
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
border: Border.all(color: const Color(0xFFA12020), width: 4),
|
border: Border.all(color: primaryDark, width: 4),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_backBar(context),
|
_backBar(context, primaryDark),
|
||||||
PokemonDetailTop(
|
PokemonDetailTop(
|
||||||
pokemon: pokemon,
|
pokemon: pokemon,
|
||||||
isShiny: _isShiny,
|
isShiny: _isShiny,
|
||||||
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
|
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
|
||||||
|
primaryColor: palette.primary,
|
||||||
|
primaryDark: primaryDark,
|
||||||
|
surfaceColor: palette.surface,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_hinge(),
|
_hinge(primaryDark),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
PokemonStatsPanel(pokemon: pokemon),
|
PokemonStatsPanel(
|
||||||
|
pokemon: pokemon,
|
||||||
|
surfaceColor: palette.surface,
|
||||||
|
descriptionOverride: _englishGenus,
|
||||||
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
_bottomDots(),
|
_bottomDots(primaryDark),
|
||||||
const SizedBox(height: 20),
|
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(
|
return Container(
|
||||||
height: 50,
|
height: 50,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
@@ -63,24 +94,24 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
|||||||
onTap: () => Navigator.pop(context),
|
onTap: () => Navigator.pop(context),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
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),
|
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _hinge() {
|
Widget _hinge(Color primaryDark) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
Container(height: 6, width: 40, color: primaryDark),
|
||||||
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
Container(height: 6, width: 40, color: primaryDark),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _bottomDots() {
|
Widget _bottomDots(Color primaryDark) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
@@ -89,7 +120,7 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
|||||||
width: 6,
|
width: 6,
|
||||||
height: 6,
|
height: 6,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
|
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../providers/pokedex_provider.dart';
|
import '../providers/pokedex_provider.dart';
|
||||||
|
import '../providers/theme_provider.dart';
|
||||||
import '../widgets/pokemon_tile.dart';
|
import '../widgets/pokemon_tile.dart';
|
||||||
import '../widgets/scanline_overlay.dart';
|
import '../widgets/scanline_overlay.dart';
|
||||||
import '../widgets/list/pokedex_list_header.dart';
|
import '../widgets/list/pokedex_list_header.dart';
|
||||||
@@ -34,6 +35,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final palette = appPalettes[ref.watch(themeProvider)];
|
||||||
final pokedexAsync = ref.watch(pokedexProvider);
|
final pokedexAsync = ref.watch(pokedexProvider);
|
||||||
final caughtCount = ref.watch(caughtCountProvider);
|
final caughtCount = ref.watch(caughtCountProvider);
|
||||||
|
|
||||||
@@ -47,8 +49,8 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab('ALL', l.tabAll, _filter == 'ALL'),
|
_buildTab('ALL', l.tabAll, _filter == 'ALL', palette.primary),
|
||||||
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT'),
|
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT', palette.primary),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -79,7 +81,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
height: 24,
|
height: 24,
|
||||||
color: const Color(0xFF1B2333),
|
color: palette.surface,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(l.pokedexFooter,
|
child: Text(l.pokedexFooter,
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
||||||
@@ -105,7 +107,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTab(String key, String label, bool isSelected) {
|
Widget _buildTab(String key, String label, bool isSelected, Color primaryColor) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -118,7 +120,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
|
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
|
||||||
border: isSelected
|
border: isSelected
|
||||||
? const Border(bottom: BorderSide(color: Color(0xFFD32F2F), width: 3))
|
? Border(bottom: BorderSide(color: primaryColor, width: 3))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../domain/game/game_engine.dart';
|
|||||||
import '../../domain/game/game_state.dart';
|
import '../../domain/game/game_state.dart';
|
||||||
import '../../core/logger.dart';
|
import '../../core/logger.dart';
|
||||||
import 'gen_filter_provider.dart';
|
import 'gen_filter_provider.dart';
|
||||||
|
import 'locale_provider.dart';
|
||||||
import 'pokedex_provider.dart';
|
import 'pokedex_provider.dart';
|
||||||
import 'repository_provider.dart';
|
import 'repository_provider.dart';
|
||||||
|
|
||||||
@@ -17,7 +18,28 @@ class GameNotifier extends Notifier<GameState> {
|
|||||||
final _random = Random();
|
final _random = Random();
|
||||||
|
|
||||||
@override
|
@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 {
|
Future<void> startNewGame() async {
|
||||||
state = _engine.newGame(state);
|
state = _engine.newGame(state);
|
||||||
@@ -35,7 +57,9 @@ class GameNotifier extends Notifier<GameState> {
|
|||||||
final repo = ref.read(pokemonRepositoryProvider);
|
final repo = ref.read(pokemonRepositoryProvider);
|
||||||
final isShiny = _random.nextInt(AppConstants.shinyOdds) == 0;
|
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 {
|
try {
|
||||||
final pokemon = await repo.getById(id);
|
final pokemon = await repo.getById(id);
|
||||||
if (pokemon == null) {
|
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.
|
/// 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.
|
/// La sélection est persistée dans les préférences.
|
||||||
class GenFilterNotifier extends Notifier<Set<int>> {
|
class GenFilterNotifier extends Notifier<Set<int>> {
|
||||||
|
late final Future<void> _ready;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<int> build() {
|
Set<int> build() {
|
||||||
_load();
|
_ready = _load();
|
||||||
// Default: all gens enabled
|
// Default: all gens enabled — overwritten by _load() once prefs are read.
|
||||||
return Set.from(List.generate(AppConstants.genRanges.length, (i) => i));
|
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 {
|
Future<void> toggle(int genIndex) async {
|
||||||
final next = Set<int>.from(state);
|
final next = Set<int>.from(state);
|
||||||
if (next.contains(genIndex)) {
|
if (next.contains(genIndex)) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class LocaleNotifier extends Notifier<Locale> {
|
|||||||
@override
|
@override
|
||||||
Locale build() {
|
Locale build() {
|
||||||
_loadSaved();
|
_loadSaved();
|
||||||
return const Locale('en');
|
return const Locale('fr');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadSaved() async {
|
Future<void> _loadSaved() async {
|
||||||
|
|||||||
@@ -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.
|
/// 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>> {
|
class PokedexNotifier extends AsyncNotifier<List<Pokemon>> {
|
||||||
Future<List<Pokemon>> _load() async {
|
Future<List<Pokemon>> _load() async {
|
||||||
final repo = ref.read(pokemonRepositoryProvider);
|
final repo = ref.watch(pokemonRepositoryProvider);
|
||||||
final list = await repo.getAll();
|
final list = await repo.getAll();
|
||||||
list.sort((a, b) => a.id.compareTo(b.id));
|
list.sort((a, b) => a.id.compareTo(b.id));
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import '../../data/datasources/pokemon_local_datasource.dart';
|
|||||||
import '../../data/datasources/pokemon_remote_datasource.dart';
|
import '../../data/datasources/pokemon_remote_datasource.dart';
|
||||||
import '../../data/repositories/pokemon_repository_impl.dart';
|
import '../../data/repositories/pokemon_repository_impl.dart';
|
||||||
import '../../domain/repositories/pokemon_repository.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 pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
|
||||||
final remote = PokemonRemoteDataSource();
|
final lang = ref.watch(localeProvider).languageCode;
|
||||||
final local = kIsWeb ? null : PokemonLocalDataSource();
|
final remote = PokemonRemoteDataSource(languageCode: lang);
|
||||||
|
final local = kIsWeb ? null : PokemonLocalDataSource(languageCode: lang);
|
||||||
return PokemonRepositoryImpl(remote: remote, local: local);
|
return PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,25 +2,21 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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 {
|
class AppPalette {
|
||||||
final String name;
|
|
||||||
final Color primary;
|
final Color primary;
|
||||||
final Color surface;
|
final Color surface;
|
||||||
|
|
||||||
const AppPalette({
|
const AppPalette({required this.primary, required this.surface});
|
||||||
required this.name,
|
|
||||||
required this.primary,
|
|
||||||
required this.surface,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const List<AppPalette> appPalettes = [
|
const List<AppPalette> appPalettes = [
|
||||||
AppPalette(name: 'Pokédex Rouge', primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
|
AppPalette(primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
|
||||||
AppPalette(name: 'Océan Bleu', primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
|
AppPalette(primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
|
||||||
AppPalette(name: 'Forêt Verte', primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
|
AppPalette(primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
|
||||||
AppPalette(name: 'Foudre Jaune', primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
|
AppPalette(primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
|
||||||
AppPalette(name: 'Ombre Violette',primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
|
AppPalette(primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
|
||||||
];
|
];
|
||||||
|
|
||||||
const String _prefsPaletteIndex = 'palette_index';
|
const String _prefsPaletteIndex = 'palette_index';
|
||||||
|
|||||||
@@ -28,8 +28,37 @@ Color typeToColor(PokemonType type) {
|
|||||||
return map[type] ?? Colors.transparent;
|
return map[type] ?? Colors.transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nom du type avec une majuscule initiale.
|
/// Nom du type localisé (EN par défaut, FR si languageCode == 'fr').
|
||||||
String formatedTypeName(PokemonType type) {
|
String localizedTypeName(PokemonType type, String languageCode) {
|
||||||
final typeName = type.name;
|
if (languageCode == 'fr') {
|
||||||
return typeName[0].toUpperCase() + typeName.substring(1);
|
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 Pokemon pokemon;
|
||||||
final bool isShiny;
|
final bool isShiny;
|
||||||
final VoidCallback onToggleShiny;
|
final VoidCallback onToggleShiny;
|
||||||
|
final Color primaryColor;
|
||||||
|
final Color primaryDark;
|
||||||
|
final Color surfaceColor;
|
||||||
|
|
||||||
const PokemonDetailTop({
|
const PokemonDetailTop({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pokemon,
|
required this.pokemon,
|
||||||
required this.isShiny,
|
required this.isShiny,
|
||||||
required this.onToggleShiny,
|
required this.onToggleShiny,
|
||||||
|
required this.primaryColor,
|
||||||
|
required this.primaryDark,
|
||||||
|
required this.surfaceColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,9 +27,9 @@ class PokemonDetailTop extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFF1B2333), width: 8),
|
border: Border.all(color: surfaceColor, width: 8),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: const Color(0xFF90A4AE),
|
color: const Color(0xFF90A4AE),
|
||||||
@@ -32,7 +38,7 @@ class PokemonDetailTop extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
color: const Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
child: Text(
|
child: Text(
|
||||||
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
|
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
@@ -60,7 +66,11 @@ class PokemonDetailTop extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
pokemon.formatedName.toUpperCase(),
|
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,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../domain/entities/pokemon.dart';
|
import '../../../domain/entities/pokemon.dart';
|
||||||
import '../../../l10n/app_localizations.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.
|
/// Écran inférieur du détail : stats de base, description localisée et éléments décoratifs.
|
||||||
class PokemonStatsPanel extends StatelessWidget {
|
class PokemonStatsPanel extends ConsumerWidget {
|
||||||
final Pokemon pokemon;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
|
ref.watch(localeProvider); // watch locale so widget rebuilds on language change
|
||||||
|
final description = descriptionOverride ?? pokemon.description;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(8),
|
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(
|
child: Container(
|
||||||
color: const Color(0xFFC8D1D8),
|
color: const Color(0xFFC8D1D8),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -23,12 +39,20 @@ class PokemonStatsPanel extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(l.baseStats, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
Text(l.baseStats,
|
||||||
Text("MODEL: DS-01", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold)),
|
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),
|
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: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)),
|
||||||
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
|
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
|
||||||
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
|
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
|
||||||
@@ -36,16 +60,19 @@ class PokemonStatsPanel extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(12),
|
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(
|
child: Text(
|
||||||
pokemon.description != null && pokemon.description!.isNotEmpty
|
description != null && description.isNotEmpty
|
||||||
? '"${pokemon.description!}"'
|
? '"$description"'
|
||||||
: '"${l.noDescription}"',
|
: '"${l.noDescription}"',
|
||||||
style: const TextStyle(fontSize: 16, height: 1.5),
|
style: const TextStyle(fontSize: 16, height: 1.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const _DecorativeLights(),
|
_DecorativeLights(accentColor: surfaceColor),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -53,7 +80,6 @@ class PokemonStatsPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Barre d'une statistique (libellé, jauge proportionnelle, valeur).
|
|
||||||
class _StatBar extends StatelessWidget {
|
class _StatBar extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final int value;
|
final int value;
|
||||||
@@ -62,20 +88,27 @@ class _StatBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 14,
|
height: 14,
|
||||||
decoration: BoxDecoration(color: Colors.grey[400]),
|
decoration: BoxDecoration(color: Colors.grey[400]),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(flex: (ratio * 100).toInt(), child: Container(color: color)),
|
Expanded(
|
||||||
Expanded(flex: 100 - (ratio * 100).toInt(), child: Container()),
|
flex: (ratio * 100).toInt(),
|
||||||
|
child: Container(color: color)),
|
||||||
|
Expanded(
|
||||||
|
flex: 100 - (ratio * 100).toInt(),
|
||||||
|
child: Container()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -84,7 +117,8 @@ class _StatBar extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: Text(value.toString(),
|
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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -92,21 +126,26 @@ class _StatBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Petites diodes décoratives en bas du panneau (purement cosmétiques).
|
|
||||||
class _DecorativeLights extends StatelessWidget {
|
class _DecorativeLights extends StatelessWidget {
|
||||||
const _DecorativeLights();
|
final Color accentColor;
|
||||||
|
const _DecorativeLights({required this.accentColor});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final dark = HSLColor.fromColor(accentColor)
|
||||||
|
.withLightness(
|
||||||
|
(HSLColor.fromColor(accentColor).lightness - 0.1).clamp(0.0, 1.0))
|
||||||
|
.toColor();
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1E88E5),
|
color: accentColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: const Color(0xFF1565C0), width: 2),
|
border: Border.all(color: dark, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -122,9 +161,19 @@ class _DecorativeLights extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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),
|
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))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,19 +3,27 @@ import '../../../l10n/app_localizations.dart';
|
|||||||
|
|
||||||
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
|
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
|
||||||
class GameOverActions extends StatelessWidget {
|
class GameOverActions extends StatelessWidget {
|
||||||
|
final Color primaryColor;
|
||||||
|
final Color primaryDark;
|
||||||
final VoidCallback onTryAgain;
|
final VoidCallback onTryAgain;
|
||||||
final VoidCallback onBack;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_button(label: l.tryAgain, icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain),
|
_button(label: l.tryAgain, icon: Icons.refresh, color: primaryColor, onPressed: onTryAgain),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_button(label: l.backToPokedex, icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack),
|
_button(label: l.backToPokedex, icon: Icons.menu_book, color: primaryDark, onPressed: onBack),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,24 @@ import '../pokemon_image.dart';
|
|||||||
class GameOverHeader extends StatelessWidget {
|
class GameOverHeader extends StatelessWidget {
|
||||||
final String pokemonImage;
|
final String pokemonImage;
|
||||||
final String pokemonName;
|
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);
|
static const _silverBg = Color(0xFFC8D1D8);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
return Container(
|
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(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
||||||
@@ -24,7 +31,7 @@ class GameOverHeader extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: _darkRed,
|
color: primaryDark,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
l.gameOver,
|
l.gameOver,
|
||||||
@@ -44,10 +51,10 @@ class GameOverHeader extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
l.itWas(pokemonName),
|
l.itWas(pokemonName),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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.
|
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
|
||||||
class HingeDivider extends StatelessWidget {
|
class HingeDivider extends StatelessWidget {
|
||||||
const HingeDivider({super.key});
|
final Color color;
|
||||||
|
const HingeDivider({super.key, required this.color});
|
||||||
static const _darkRed = Color(0xFF9E1B1B);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -12,7 +11,7 @@ class HingeDivider extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Container(height: 2, color: _darkRed)),
|
Expanded(child: Container(height: 2, color: color)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -22,12 +21,12 @@ class HingeDivider extends StatelessWidget {
|
|||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
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),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Container(height: 2, color: _darkRed)),
|
Expanded(child: Container(height: 2, color: color)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class GenFilterSection extends StatelessWidget {
|
|||||||
final VoidCallback onToggleOpen;
|
final VoidCallback onToggleOpen;
|
||||||
final Set<int> selectedGens;
|
final Set<int> selectedGens;
|
||||||
final void Function(int) onToggle;
|
final void Function(int) onToggle;
|
||||||
|
final Color surfaceColor;
|
||||||
|
|
||||||
const GenFilterSection({
|
const GenFilterSection({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -15,6 +16,7 @@ class GenFilterSection extends StatelessWidget {
|
|||||||
required this.onToggleOpen,
|
required this.onToggleOpen,
|
||||||
required this.selectedGens,
|
required this.selectedGens,
|
||||||
required this.onToggle,
|
required this.onToggle,
|
||||||
|
required this.surfaceColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,19 +31,19 @@ class GenFilterSection extends StatelessWidget {
|
|||||||
onPressed: onToggleOpen,
|
onPressed: onToggleOpen,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isOpen ? Icons.expand_less : Icons.filter_list,
|
isOpen ? Icons.expand_less : Icons.filter_list,
|
||||||
color: const Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context)!.genFilter,
|
AppLocalizations.of(context)!.genFilter,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
|
side: BorderSide(color: surfaceColor, width: 2),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
),
|
),
|
||||||
@@ -51,7 +53,7 @@ class GenFilterSection extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
child: isOpen
|
child: isOpen
|
||||||
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle)
|
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle, surfaceColor: surfaceColor)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -63,8 +65,9 @@ class GenFilterSection extends StatelessWidget {
|
|||||||
class _GenFilterPanel extends StatelessWidget {
|
class _GenFilterPanel extends StatelessWidget {
|
||||||
final Set<int> selectedGens;
|
final Set<int> selectedGens;
|
||||||
final void Function(int) onToggle;
|
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 = [
|
static const _genNames = [
|
||||||
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
|
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
|
||||||
@@ -76,7 +79,7 @@ class _GenFilterPanel extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(top: 4),
|
margin: const EdgeInsets.only(top: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(color: const Color(0xFF1B2333), width: 2),
|
border: Border.all(color: surfaceColor, width: 2),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: List.generate(AppConstants.genRanges.length, (i) {
|
children: List.generate(AppConstants.genRanges.length, (i) {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ class GuessSilhouette extends StatelessWidget {
|
|||||||
final Pokemon pokemon;
|
final Pokemon pokemon;
|
||||||
final bool isShiny;
|
final bool isShiny;
|
||||||
final bool isGuessed;
|
final bool isGuessed;
|
||||||
|
final Color surfaceColor;
|
||||||
|
|
||||||
const GuessSilhouette({
|
const GuessSilhouette({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pokemon,
|
required this.pokemon,
|
||||||
required this.isShiny,
|
required this.isShiny,
|
||||||
required this.isGuessed,
|
required this.isGuessed,
|
||||||
|
required this.surfaceColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -27,7 +29,7 @@ class GuessSilhouette extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF3B6EE3),
|
color: const Color(0xFF3B6EE3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFF1B2333), width: 8),
|
border: Border.all(color: surfaceColor, width: 8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -51,7 +53,7 @@ class GuessSilhouette extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: const Color(0xFF1B2333),
|
color: surfaceColor,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -10,15 +10,9 @@ class PokedexListHeader extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
color: const Color(0xFF90A4AE),
|
color: const Color(0xFF90A4AE),
|
||||||
child: Row(
|
alignment: Alignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
child: Text(AppLocalizations.of(context)!.listNational,
|
||||||
children: [
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
const Icon(Icons.menu, color: Colors.black87),
|
|
||||||
Text(AppLocalizations.of(context)!.listNational,
|
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
|
||||||
const Icon(Icons.search, color: Colors.black87),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class PokemonTypeWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String typeName = formatedTypeName(type);
|
final lang = Localizations.localeOf(context).languageCode;
|
||||||
|
String typeName = localizedTypeName(type, lang);
|
||||||
Color typeColor = typeToColor(type);
|
Color typeColor = typeToColor(type);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
import '../../providers/locale_provider.dart';
|
import '../../providers/locale_provider.dart';
|
||||||
|
|
||||||
/// Sélecteur de langue de l'application (parmi [supportedLocales]).
|
/// Sélecteur de langue de l'application (parmi [supportedLocales]).
|
||||||
@@ -14,11 +15,13 @@ class LanguagePicker extends StatelessWidget {
|
|||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const _names = {'en': 'English', 'fr': 'Français'};
|
|
||||||
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
|
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final names = {'en': l.langEnglish, 'fr': l.langFrench};
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: supportedLocales.map((locale) {
|
children: supportedLocales.map((locale) {
|
||||||
final isSelected = locale.languageCode == selected.languageCode;
|
final isSelected = locale.languageCode == selected.languageCode;
|
||||||
@@ -41,7 +44,7 @@ class LanguagePicker extends StatelessWidget {
|
|||||||
Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)),
|
Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
_names[locale.languageCode] ?? locale.languageCode,
|
names[locale.languageCode] ?? locale.languageCode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
import '../../providers/theme_provider.dart';
|
import '../../providers/theme_provider.dart';
|
||||||
|
|
||||||
/// Sélecteur de palette de couleurs de l'application.
|
/// Sélecteur de palette de couleurs de l'application.
|
||||||
@@ -9,6 +10,9 @@ class PalettePicker extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final paletteNames = [l.palette0, l.palette1, l.palette2, l.palette3, l.palette4];
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: List.generate(appPalettes.length, (i) {
|
children: List.generate(appPalettes.length, (i) {
|
||||||
final p = appPalettes[i];
|
final p = appPalettes[i];
|
||||||
@@ -53,7 +57,7 @@ class PalettePicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
p.name,
|
paletteNames[i],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user