Compare commits
No commits in common. "main" and "pokeguess-preview" have entirely different histories.
main
...
pokeguess-
95
README.md
95
README.md
@ -1,82 +1,37 @@
|
|||||||
# Pokéguess
|
# pokedex
|
||||||
|
|
||||||
> Devine le Pokémon à partir de sa silhouette !
|
Projet d'exemple d'application Flutter pour les cours de bootcamp Ynov 2023.
|
||||||
|
Ce projet contient un début de pokédex, une application listant des pokémons et permettant d'en voir les détails.
|
||||||
Application mobile Flutter développée dans le cadre du cours **DEV MOBILE** (Ynov).
|
|
||||||
À l'origine basée sur l'exemple de pokédex du bootcamp, le projet est devenu un jeu
|
|
||||||
complet « Who's that Pokémon ? » adossé à un pokédex.
|
|
||||||
|
|
||||||
## Fonctionnalités
|
|
||||||
|
|
||||||
- **Jeu de devinette** : une silhouette de Pokémon à identifier, avec un système de
|
|
||||||
vies, d'indices et de passes (skips), des Pokémon **shiny** (bonus de points), un
|
|
||||||
score courant et un meilleur score persistant.
|
|
||||||
- **Filtre par génération** : on choisit les générations (I à IX) dont les Pokémon
|
|
||||||
peuvent tomber pendant le jeu.
|
|
||||||
- **Pokédex** : liste nationale des Pokémon avec filtre `ALL` / `CAUGHT` et compteur de
|
|
||||||
progression (découverts / total).
|
|
||||||
- **Fiche détaillée** : statistiques de base, description, types, bascule normal/shiny.
|
|
||||||
- **Page Système** : statistiques globales (attrapés, vus, complétion, meilleur score)
|
|
||||||
et choix de la **palette de couleurs** de l'app.
|
|
||||||
- **Offline-first** : les données sont mises en cache localement (SQLite) ; l'app reste
|
|
||||||
utilisable hors-ligne une fois la synchronisation initiale faite.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Le projet suit une **Clean Architecture allégée** en trois couches, avec **Riverpod**
|
|
||||||
pour la gestion d'état :
|
|
||||||
|
|
||||||
```text
|
|
||||||
lib/
|
|
||||||
├── core/ # constantes, logger (Dart pur)
|
|
||||||
├── domain/ # entités, interface de repository, moteur de jeu (Dart pur, testé)
|
|
||||||
├── data/ # DTO de parsing, datasources SQLite/HTTP, implémentation du repository
|
|
||||||
└── presentation/ # providers Riverpod, pages et widgets (chacun < 150 lignes)
|
|
||||||
```
|
|
||||||
|
|
||||||
La règle de dépendance pointe vers l'intérieur : `domain` ne dépend de rien,
|
|
||||||
`data` et `presentation` dépendent de `domain`. Détails dans
|
|
||||||
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
||||||
|
|
||||||
## Stack technique
|
|
||||||
|
|
||||||
- [Flutter](https://flutter.dev) / Dart
|
|
||||||
- [flutter_riverpod](https://pub.dev/packages/flutter_riverpod) — gestion d'état
|
|
||||||
- [sqflite](https://pub.dev/packages/sqflite) — persistance locale (SQLite)
|
|
||||||
- [http](https://pub.dev/packages/http) — appels réseau
|
|
||||||
- [shared_preferences](https://pub.dev/packages/shared_preferences) — préférences (meilleur score, palette)
|
|
||||||
- [google_fonts](https://pub.dev/packages/google_fonts) — police rétro
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://gitea.losvernos.com/guillaumev/quel-est-ce-pokemon.git
|
git clone https://github.com/Efilown/pokedex-example.git
|
||||||
cd quel-est-ce-pokemon
|
cd pokedex-example
|
||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
|
|
||||||
Puis lancer l'application sur le device de votre choix (téléphone, émulateur ou
|
Ensuite, ouvrir le projet avec un IDE (tel que vscode) et le lancer sur le device de votre choix (téléphone, émulateur ou navigateur).
|
||||||
navigateur) :
|
|
||||||
|
|
||||||
```sh
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
```sh
|
|
||||||
flutter test
|
|
||||||
```
|
|
||||||
|
|
||||||
Les tests couvrent le moteur de jeu (`GameEngine`), le parsing (`PokemonDto`) et le
|
|
||||||
repository.
|
|
||||||
|
|
||||||
## Ressources utilisées
|
## Ressources utilisées
|
||||||
|
|
||||||
- [API Tyradex](https://tyradex.app) — données des Pokémon (français)
|
Pour les images (remplacer par l'id du pokémon désiré):
|
||||||
- Sprites officiels via le dépôt [TyraDex/sprites](https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/)
|
|
||||||
(remplacer `<id>` par le numéro du Pokémon) :
|
|
||||||
- normal : `.../sprites/<id>/regular.png`
|
|
||||||
- shiny : `.../sprites/<id>/shiny.png`
|
|
||||||
- [Cris des Pokémon](https://pokemoncries.com/cries/1.mp3)
|
|
||||||
|
|
||||||
|
- [Sprite normal](https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png)
|
||||||
|
- [Sprite shiny](https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/6.png)
|
||||||
|
|
||||||
|
Autres liens
|
||||||
|
|
||||||
|
- [l'API pour récupérer les informations](https://pokeapi.co/api/v2/pokemon/6)
|
||||||
|
- [Les cris de bataille](https://pokemoncries.com/cries/1.mp3)
|
||||||
|
- [La documentation du paquet SQFlite](https://pub.dev/packages/sqflite)
|
||||||
|
- [La documentation du paquet HTTP](https://pub.dev/packages/http)
|
||||||
|
|
||||||
|
## Améliorations possible
|
||||||
|
|
||||||
|
- Mettre un lecteur audio pour écouter les cris des pokémons
|
||||||
|
- Rajouter la traduction (en fonction de la langue de l'application ? ☺)
|
||||||
|
- Compléter la vue détail avec des informations tirées de l'API
|
||||||
|
- Faire une vue "mon équipe" contenant 6 pokémons choisis en rajoutant un bouton "ajouter à mon équipe" sur la vue détail.
|
||||||
|
|
||||||
|
Amusez-vous !
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -8,74 +8,23 @@ 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).
|
||||||
- `fromTyradexJson()`, `fromDb()`, `toDb()`.
|
- **Datasources** : `PokemonLocalDataSource` (SQLite via sqflite), `PokemonRemoteDataSource` (HTTP).
|
||||||
- **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`),
|
||||||
#### Providers (Riverpod)
|
`gameProvider` (`Notifier<GameState>`), `selectedTabProvider` (onglet courant).
|
||||||
|
- **Pages** : `ConsumerWidget` / `ConsumerStatefulWidget` qui observent les providers.
|
||||||
| Provider | Type | Rôle |
|
- **Widgets** : éléments réutilisables (`PokemonImage`, `PokemonTile`, `PokemonTypeWidget`).
|
||||||
| --- | --- | --- |
|
- **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
|
||||||
|
|
||||||
@ -87,19 +36,8 @@ 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 JSON ↔ DB.
|
- `test/data/pokemon_dto_test.dart` : parsing.
|
||||||
- `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.
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -2,27 +2,15 @@
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokémon through
|
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.
|
||||||
a silhouette guessing game. The app fetches data from the Tyradex API and stores it locally for
|
|
||||||
offline access.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **National Pokédex**: Browse all 1025 Pokémon from generations I to IX.
|
- National Pokedex: Browse all 1025+ Pokemon from all generations.
|
||||||
- **Guess Game**: Identify Pokémon by their silhouette.
|
- Guess Game: Identify Pokemon by their silhouette.
|
||||||
- **Scoring System**: Earn 10 pts per correct guess, 20 pts for Shiny Pokémon (1 in 10 chance).
|
- Scoring System: Earn points for correct guesses, with bonuses for Shiny Pokemon. High scores are saved locally.
|
||||||
High scores are saved locally.
|
- Collection: Track caught and seen Pokemon.
|
||||||
- **Lives, Hints & Skips**: Start each game with 3 lives, 3 hints, and 3 skips. Earn bonuses
|
- Search and Filter: Filter the collection by all or caught status and search by name.
|
||||||
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
|
||||||
|
|
||||||
@ -33,11 +21,8 @@ offline access.
|
|||||||
|
|
||||||
## Technologies
|
## Technologies
|
||||||
|
|
||||||
- **Flutter**: UI Framework.
|
- Flutter: UI Framework.
|
||||||
- **Riverpod**: State management (providers + notifiers).
|
- SQLite (sqflite): Local database.
|
||||||
- **SQLite (sqflite)**: Local database for offline-first Pokémon storage.
|
- Tyradex API: Pokemon data source.
|
||||||
- **Tyradex API**: Primary Pokémon data source (FR/EN names, sprites, types, stats).
|
- Shared Preferences: High score persistence.
|
||||||
- **PokéAPI**: Secondary source for English genus data.
|
- Google Fonts: Custom typography.
|
||||||
- **Shared Preferences**: Persistence for best score, generation filter, language, and palette.
|
|
||||||
- **Google Fonts**: Custom typography (VT323).
|
|
||||||
- **flutter_localizations / intl**: FR and EN localization.
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 775 KiB |
@ -1,4 +0,0 @@
|
|||||||
arb-dir: lib/l10n
|
|
||||||
template-arb-file: app_en.arb
|
|
||||||
output-localization-file: app_localizations.dart
|
|
||||||
output-dir: lib/l10n
|
|
||||||
@ -41,9 +41,6 @@ class AppConstants {
|
|||||||
/// Clé SharedPreferences pour le filtre de générations.
|
/// Clé SharedPreferences pour le filtre de générations.
|
||||||
static const String prefsGenFilter = 'gen_filter';
|
static const String prefsGenFilter = 'gen_filter';
|
||||||
|
|
||||||
/// Clé SharedPreferences pour la langue de l'application.
|
|
||||||
static const String prefsLocale = 'locale_code';
|
|
||||||
|
|
||||||
/// Plages d'IDs Pokémon par génération [min, max] (inclusif).
|
/// Plages d'IDs Pokémon par génération [min, max] (inclusif).
|
||||||
static const List<(int, int)> genRanges = [
|
static const List<(int, int)> genRanges = [
|
||||||
(1, 151), // Gen 1
|
(1, 151), // Gen 1
|
||||||
|
|||||||
@ -2,43 +2,24 @@ 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';
|
||||||
|
|
||||||
const _createSql = '''
|
/// Accès SQLite local au Pokédex. Schéma et migrations identiques à l'ancien PokedexDatabase.
|
||||||
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: 5,
|
version: 2,
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
// Drop and recreate on any upgrade to pick up new columns cleanly.
|
if (oldVersion < 2) {
|
||||||
await db.execute('DROP TABLE IF EXISTS pokemon');
|
await db.execute('DROP TABLE IF EXISTS pokemon');
|
||||||
await db.execute(_createSql);
|
await db.execute(
|
||||||
|
'CREATE TABLE pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onCreate: (db, version) async {
|
onCreate: (db, version) async {
|
||||||
await db.execute(_createSql);
|
await db.execute(
|
||||||
|
'CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,14 +27,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((r) => PokemonDto.fromDb(r, languageCode: languageCode)).toList();
|
return rows.map(PokemonDto.fromDb).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, languageCode: languageCode);
|
return PokemonDto.fromDb(rows.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveAll(List<Pokemon> pokemons) async {
|
Future<void> saveAll(List<Pokemon> pokemons) async {
|
||||||
|
|||||||
@ -8,13 +8,12 @@ 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, this.languageCode = 'fr'})
|
PokemonRemoteDataSource({http.Client? client})
|
||||||
: _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 (lang: $languageCode)');
|
AppLogger.info('API: fetching Pokémon $id');
|
||||||
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) {
|
||||||
@ -22,29 +21,11 @@ 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,
|
return PokemonDto.fromTyradexJson(json, fallbackId: id);
|
||||||
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 (lang: $languageCode)');
|
AppLogger.info('API: fetching ALL Pokémon');
|
||||||
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) {
|
||||||
@ -53,12 +34,9 @@ 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;
|
if (json['pokedex_id'] == 0) continue; // entrée générique Tyradex
|
||||||
try {
|
try {
|
||||||
result.add(PokemonDto.fromTyradexJson(
|
result.add(PokemonDto.fromTyradexJson(json as Map<String, dynamic>));
|
||||||
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,7 +4,8 @@ import '../../domain/entities/pokemon.dart';
|
|||||||
class PokemonDto {
|
class PokemonDto {
|
||||||
PokemonDto._();
|
PokemonDto._();
|
||||||
|
|
||||||
static PokemonType _frenchTypeToEnum(String type) {
|
/// Mappe un nom de type français (API Tyradex) vers l'enum.
|
||||||
|
static PokemonType frenchTypeToEnum(String frenchType) {
|
||||||
const map = {
|
const map = {
|
||||||
'Normal': PokemonType.normal,
|
'Normal': PokemonType.normal,
|
||||||
'Combat': PokemonType.fighting,
|
'Combat': PokemonType.fighting,
|
||||||
@ -25,65 +26,41 @@ class PokemonDto {
|
|||||||
'Ténèbres': PokemonType.dark,
|
'Ténèbres': PokemonType.dark,
|
||||||
'Fée': PokemonType.fairy,
|
'Fée': PokemonType.fairy,
|
||||||
};
|
};
|
||||||
return map[type] ?? PokemonType.unknown;
|
return map[frenchType] ?? PokemonType.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tyradex always returns type names in French regardless of endpoint language.
|
/// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste).
|
||||||
static PokemonType _typeToEnum(String typeName, String languageCode) {
|
/// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`.
|
||||||
return _frenchTypeToEnum(typeName);
|
static Pokemon fromTyradexJson(Map<String, dynamic> json, {int? fallbackId}) {
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 nameFr = nameMap?['fr'] as String? ?? 'unknown';
|
final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown';
|
||||||
final nameEn = nameMap?['en'] as String? ?? nameFr;
|
|
||||||
final name = languageCode == 'en' ? nameEn : nameFr;
|
|
||||||
|
|
||||||
final List types = json['types'] ?? [];
|
final List types = json['types'] ?? [];
|
||||||
// Tyradex retourne toujours les types dans la langue du endpoint (fr ou en).
|
final type1 =
|
||||||
final type1 = types.isNotEmpty
|
types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown;
|
||||||
? _typeToEnum(types[0]['name'] as String, languageCode)
|
final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null;
|
||||||
: 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,
|
spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex
|
||||||
description: descFr,
|
description: json['category'],
|
||||||
descriptionEn: descEn,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sérialise pour SQLite (stocke les deux noms et descriptions).
|
/// Sérialise pour SQLite.
|
||||||
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,
|
||||||
@ -92,23 +69,15 @@ 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.
|
||||||
/// [languageCode] détermine quel nom afficher.
|
static Pokemon fromDb(Map<String, dynamic> row) {
|
||||||
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: name,
|
name: row['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'],
|
||||||
@ -125,7 +94,6 @@ 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,7 +60,4 @@ 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,8 +1,6 @@
|
|||||||
/// 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;
|
||||||
@ -11,14 +9,11 @@ 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,
|
||||||
@ -27,7 +22,6 @@ 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,
|
||||||
});
|
});
|
||||||
@ -43,8 +37,6 @@ 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,
|
||||||
@ -53,14 +45,11 @@ 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,
|
||||||
@ -69,7 +58,6 @@ 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,15 +37,9 @@ class GameEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final normalizedGuess = _normalize(guess.trim().toLowerCase());
|
final normalizedGuess = _normalize(guess.trim().toLowerCase());
|
||||||
|
final normalizedActual = _normalize(pokemon.name.toLowerCase());
|
||||||
|
|
||||||
// Accept the name in either language so a language switch mid-game never blocks the player.
|
if (normalizedGuess == normalizedActual) {
|
||||||
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,7 +19,4 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
{
|
|
||||||
"@@locale": "en",
|
|
||||||
"appTitle": "Pokéguess",
|
|
||||||
|
|
||||||
"whosThatPokemon": "WHO'S THAT POKÉMON?",
|
|
||||||
"shinyDetected": "✨ SHINY POKÉMON DETECTED! ✨",
|
|
||||||
"genFilter": "GEN FILTER",
|
|
||||||
"identificationInput": "IDENTIFICATION INPUT",
|
|
||||||
"hintPrefix": "HINT",
|
|
||||||
"enterPokemonName": "Enter Pokémon name...",
|
|
||||||
"continueButton": "CONTINUE",
|
|
||||||
"guessButton": "GUESS!",
|
|
||||||
"hintButton": "HINT ({count})",
|
|
||||||
"@hintButton": { "placeholders": { "count": { "type": "int" } } },
|
|
||||||
"skipButton": "SKIP ({count})",
|
|
||||||
"@skipButton": { "placeholders": { "count": { "type": "int" } } },
|
|
||||||
"currentScore": "CURRENT SCORE",
|
|
||||||
"personalBest": "PERSONAL BEST: {score}",
|
|
||||||
"@personalBest": { "placeholders": { "score": { "type": "int" } } },
|
|
||||||
"caughtShiny": "✨ SHINY! You caught {name}! (+20 pts) ✨",
|
|
||||||
"@caughtShiny": { "placeholders": { "name": { "type": "String" } } },
|
|
||||||
"caughtNormal": "Correct! You caught {name}!",
|
|
||||||
"@caughtNormal": { "placeholders": { "name": { "type": "String" } } },
|
|
||||||
"wrongGuess": "Wrong guess! Try again.",
|
|
||||||
"errorLoadingPokemon": "Error loading Pokémon",
|
|
||||||
|
|
||||||
"listNational": "LIST - NATIONAL",
|
|
||||||
"tabAll": "ALL",
|
|
||||||
"tabCaught": "CAUGHT",
|
|
||||||
"pokemonDiscovered": "POKEMON DISCOVERED",
|
|
||||||
"noPokemonFound": "NO POKEMON FOUND IN {filter}",
|
|
||||||
"@noPokemonFound": { "placeholders": { "filter": { "type": "String" } } },
|
|
||||||
"pokedexFooter": "NATIONAL POKEDEX V2.0",
|
|
||||||
"loadingError": "Loading error",
|
|
||||||
|
|
||||||
"baseStats": "BASE STATS",
|
|
||||||
"noDescription": "No description available for this Pokémon.",
|
|
||||||
|
|
||||||
"gameOver": "GAME OVER",
|
|
||||||
"itWas": "It was {name}!",
|
|
||||||
"@itWas": { "placeholders": { "name": { "type": "String" } } },
|
|
||||||
"gameOverMessage": "\"Looks like your journey ends here. You've run out of energy!\"",
|
|
||||||
"statStreak": "STREAK",
|
|
||||||
"statSeen": "SEEN",
|
|
||||||
"statScore": "SCORE",
|
|
||||||
"tryAgain": "TRY AGAIN",
|
|
||||||
"backToPokedex": "BACK TO POKEDEX",
|
|
||||||
|
|
||||||
"system": "SYSTEM",
|
|
||||||
"statistics": "STATISTICS",
|
|
||||||
"colorPalette": "COLOR PALETTE",
|
|
||||||
"language": "LANGUAGE",
|
|
||||||
"statBestScore": "Best score",
|
|
||||||
"statCaught": "Caught",
|
|
||||||
"statSeenLabel": "Seen",
|
|
||||||
"statCompletion": "Completion",
|
|
||||||
|
|
||||||
"navList": "LIST",
|
|
||||||
"navGuess": "GUESS",
|
|
||||||
"navSystem": "SYSTEM",
|
|
||||||
|
|
||||||
"langEnglish": "English",
|
|
||||||
"langFrench": "French",
|
|
||||||
|
|
||||||
"palette0": "Pokédex Red",
|
|
||||||
"palette1": "Blue Ocean",
|
|
||||||
"palette2": "Green Forest",
|
|
||||||
"palette3": "Lightning Yellow",
|
|
||||||
"palette4": "Purple Shadow"
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"@@locale": "fr",
|
|
||||||
"appTitle": "Pokéguess",
|
|
||||||
|
|
||||||
"whosThatPokemon": "QUI EST CE POKÉMON ?",
|
|
||||||
"shinyDetected": "✨ POKÉMON SHINY DÉTECTÉ ! ✨",
|
|
||||||
"genFilter": "FILTRE GÉN.",
|
|
||||||
"identificationInput": "SAISIE D'IDENTIFICATION",
|
|
||||||
"hintPrefix": "INDICE",
|
|
||||||
"enterPokemonName": "Entrez le nom du Pokémon...",
|
|
||||||
"continueButton": "CONTINUER",
|
|
||||||
"guessButton": "DEVINER !",
|
|
||||||
"hintButton": "INDICE ({count})",
|
|
||||||
"skipButton": "PASSER ({count})",
|
|
||||||
"currentScore": "SCORE ACTUEL",
|
|
||||||
"personalBest": "MEILLEUR SCORE : {score}",
|
|
||||||
"caughtShiny": "✨ SHINY ! Tu as attrapé {name} ! (+20 pts) ✨",
|
|
||||||
"caughtNormal": "Correct ! Tu as attrapé {name} !",
|
|
||||||
"wrongGuess": "Mauvaise réponse ! Réessaie.",
|
|
||||||
"errorLoadingPokemon": "Erreur de chargement du Pokémon",
|
|
||||||
|
|
||||||
"listNational": "LISTE - NATIONALE",
|
|
||||||
"tabAll": "TOUS",
|
|
||||||
"tabCaught": "ATTRAPÉS",
|
|
||||||
"pokemonDiscovered": "POKÉMON DÉCOUVERTS",
|
|
||||||
"noPokemonFound": "AUCUN POKÉMON DANS {filter}",
|
|
||||||
"pokedexFooter": "POKÉDEX NATIONAL V2.0",
|
|
||||||
"loadingError": "Erreur de chargement",
|
|
||||||
|
|
||||||
"baseStats": "STATS DE BASE",
|
|
||||||
"noDescription": "Aucune description disponible pour ce Pokémon.",
|
|
||||||
|
|
||||||
"gameOver": "PARTIE TERMINÉE",
|
|
||||||
"itWas": "C'était {name} !",
|
|
||||||
"gameOverMessage": "« On dirait que ton aventure s'arrête ici. Tu n'as plus d'énergie ! »",
|
|
||||||
"statStreak": "SÉRIE",
|
|
||||||
"statSeen": "VUS",
|
|
||||||
"statScore": "SCORE",
|
|
||||||
"tryAgain": "REJOUER",
|
|
||||||
"backToPokedex": "RETOUR AU POKÉDEX",
|
|
||||||
|
|
||||||
"system": "SYSTÈME",
|
|
||||||
"statistics": "STATISTIQUES",
|
|
||||||
"colorPalette": "PALETTE DE COULEURS",
|
|
||||||
"language": "LANGUE",
|
|
||||||
"statBestScore": "Meilleur score",
|
|
||||||
"statCaught": "Attrapés",
|
|
||||||
"statSeenLabel": "Vus",
|
|
||||||
"statCompletion": "Complétion",
|
|
||||||
|
|
||||||
"navList": "LISTE",
|
|
||||||
"navGuess": "DEVINER",
|
|
||||||
"navSystem": "SYSTÈME",
|
|
||||||
|
|
||||||
"langEnglish": "Anglais",
|
|
||||||
"langFrench": "Français",
|
|
||||||
|
|
||||||
"palette0": "Pokédex Rouge",
|
|
||||||
"palette1": "Océan Bleu",
|
|
||||||
"palette2": "Forêt Verte",
|
|
||||||
"palette3": "Foudre Jaune",
|
|
||||||
"palette4": "Ombre Violette"
|
|
||||||
}
|
|
||||||
@ -1,445 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
||||||
import 'package:intl/intl.dart' as intl;
|
|
||||||
|
|
||||||
import 'app_localizations_en.dart';
|
|
||||||
import 'app_localizations_fr.dart';
|
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
|
||||||
/// returned by `AppLocalizations.of(context)`.
|
|
||||||
///
|
|
||||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
|
||||||
/// `localizationDelegates` list, and the locales they support in the app's
|
|
||||||
/// `supportedLocales` list. For example:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// import 'l10n/app_localizations.dart';
|
|
||||||
///
|
|
||||||
/// return MaterialApp(
|
|
||||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
||||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
/// home: MyApplicationHome(),
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Update pubspec.yaml
|
|
||||||
///
|
|
||||||
/// Please make sure to update your pubspec.yaml to include the following
|
|
||||||
/// packages:
|
|
||||||
///
|
|
||||||
/// ```yaml
|
|
||||||
/// dependencies:
|
|
||||||
/// # Internationalization support.
|
|
||||||
/// flutter_localizations:
|
|
||||||
/// sdk: flutter
|
|
||||||
/// intl: any # Use the pinned version from flutter_localizations
|
|
||||||
///
|
|
||||||
/// # Rest of dependencies
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## iOS Applications
|
|
||||||
///
|
|
||||||
/// iOS applications define key application metadata, including supported
|
|
||||||
/// locales, in an Info.plist file that is built into the application bundle.
|
|
||||||
/// To configure the locales supported by your app, you’ll need to edit this
|
|
||||||
/// file.
|
|
||||||
///
|
|
||||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
|
||||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
|
||||||
/// project’s Runner folder.
|
|
||||||
///
|
|
||||||
/// Next, select the Information Property List item, select Add Item from the
|
|
||||||
/// Editor menu, then select Localizations from the pop-up menu.
|
|
||||||
///
|
|
||||||
/// Select and expand the newly-created Localizations item then, for each
|
|
||||||
/// locale your application supports, add a new item and select the locale
|
|
||||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
|
||||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
|
||||||
/// property.
|
|
||||||
abstract class AppLocalizations {
|
|
||||||
AppLocalizations(String locale)
|
|
||||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
|
||||||
|
|
||||||
final String localeName;
|
|
||||||
|
|
||||||
static AppLocalizations? of(BuildContext context) {
|
|
||||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
|
||||||
_AppLocalizationsDelegate();
|
|
||||||
|
|
||||||
/// A list of this localizations delegate along with the default localizations
|
|
||||||
/// delegates.
|
|
||||||
///
|
|
||||||
/// Returns a list of localizations delegates containing this delegate along with
|
|
||||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
|
||||||
/// and GlobalWidgetsLocalizations.delegate.
|
|
||||||
///
|
|
||||||
/// Additional delegates can be added by appending to this list in
|
|
||||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
|
||||||
/// of delegates is preferred or required.
|
|
||||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
|
||||||
<LocalizationsDelegate<dynamic>>[
|
|
||||||
delegate,
|
|
||||||
GlobalMaterialLocalizations.delegate,
|
|
||||||
GlobalCupertinoLocalizations.delegate,
|
|
||||||
GlobalWidgetsLocalizations.delegate,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// A list of this localizations delegate's supported locales.
|
|
||||||
static const List<Locale> supportedLocales = <Locale>[
|
|
||||||
Locale('en'),
|
|
||||||
Locale('fr')
|
|
||||||
];
|
|
||||||
|
|
||||||
/// No description provided for @appTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Pokéguess'**
|
|
||||||
String get appTitle;
|
|
||||||
|
|
||||||
/// No description provided for @whosThatPokemon.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'WHO\'S THAT POKÉMON?'**
|
|
||||||
String get whosThatPokemon;
|
|
||||||
|
|
||||||
/// No description provided for @shinyDetected.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'✨ SHINY POKÉMON DETECTED! ✨'**
|
|
||||||
String get shinyDetected;
|
|
||||||
|
|
||||||
/// No description provided for @genFilter.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'GEN FILTER'**
|
|
||||||
String get genFilter;
|
|
||||||
|
|
||||||
/// No description provided for @identificationInput.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'IDENTIFICATION INPUT'**
|
|
||||||
String get identificationInput;
|
|
||||||
|
|
||||||
/// No description provided for @hintPrefix.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'HINT'**
|
|
||||||
String get hintPrefix;
|
|
||||||
|
|
||||||
/// No description provided for @enterPokemonName.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Enter Pokémon name...'**
|
|
||||||
String get enterPokemonName;
|
|
||||||
|
|
||||||
/// No description provided for @continueButton.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'CONTINUE'**
|
|
||||||
String get continueButton;
|
|
||||||
|
|
||||||
/// No description provided for @guessButton.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'GUESS!'**
|
|
||||||
String get guessButton;
|
|
||||||
|
|
||||||
/// No description provided for @hintButton.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'HINT ({count})'**
|
|
||||||
String hintButton(int count);
|
|
||||||
|
|
||||||
/// No description provided for @skipButton.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'SKIP ({count})'**
|
|
||||||
String skipButton(int count);
|
|
||||||
|
|
||||||
/// No description provided for @currentScore.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'CURRENT SCORE'**
|
|
||||||
String get currentScore;
|
|
||||||
|
|
||||||
/// No description provided for @personalBest.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'PERSONAL BEST: {score}'**
|
|
||||||
String personalBest(int score);
|
|
||||||
|
|
||||||
/// No description provided for @caughtShiny.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'✨ SHINY! You caught {name}! (+20 pts) ✨'**
|
|
||||||
String caughtShiny(String name);
|
|
||||||
|
|
||||||
/// No description provided for @caughtNormal.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Correct! You caught {name}!'**
|
|
||||||
String caughtNormal(String name);
|
|
||||||
|
|
||||||
/// No description provided for @wrongGuess.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Wrong guess! Try again.'**
|
|
||||||
String get wrongGuess;
|
|
||||||
|
|
||||||
/// No description provided for @errorLoadingPokemon.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Error loading Pokémon'**
|
|
||||||
String get errorLoadingPokemon;
|
|
||||||
|
|
||||||
/// No description provided for @listNational.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'LIST - NATIONAL'**
|
|
||||||
String get listNational;
|
|
||||||
|
|
||||||
/// No description provided for @tabAll.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'ALL'**
|
|
||||||
String get tabAll;
|
|
||||||
|
|
||||||
/// No description provided for @tabCaught.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'CAUGHT'**
|
|
||||||
String get tabCaught;
|
|
||||||
|
|
||||||
/// No description provided for @pokemonDiscovered.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'POKEMON DISCOVERED'**
|
|
||||||
String get pokemonDiscovered;
|
|
||||||
|
|
||||||
/// No description provided for @noPokemonFound.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'NO POKEMON FOUND IN {filter}'**
|
|
||||||
String noPokemonFound(String filter);
|
|
||||||
|
|
||||||
/// No description provided for @pokedexFooter.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'NATIONAL POKEDEX V2.0'**
|
|
||||||
String get pokedexFooter;
|
|
||||||
|
|
||||||
/// No description provided for @loadingError.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Loading error'**
|
|
||||||
String get loadingError;
|
|
||||||
|
|
||||||
/// No description provided for @baseStats.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'BASE STATS'**
|
|
||||||
String get baseStats;
|
|
||||||
|
|
||||||
/// No description provided for @noDescription.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'No description available for this Pokémon.'**
|
|
||||||
String get noDescription;
|
|
||||||
|
|
||||||
/// No description provided for @gameOver.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'GAME OVER'**
|
|
||||||
String get gameOver;
|
|
||||||
|
|
||||||
/// No description provided for @itWas.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'It was {name}!'**
|
|
||||||
String itWas(String name);
|
|
||||||
|
|
||||||
/// No description provided for @gameOverMessage.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'\"Looks like your journey ends here. You\'ve run out of energy!\"'**
|
|
||||||
String get gameOverMessage;
|
|
||||||
|
|
||||||
/// No description provided for @statStreak.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'STREAK'**
|
|
||||||
String get statStreak;
|
|
||||||
|
|
||||||
/// No description provided for @statSeen.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'SEEN'**
|
|
||||||
String get statSeen;
|
|
||||||
|
|
||||||
/// No description provided for @statScore.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'SCORE'**
|
|
||||||
String get statScore;
|
|
||||||
|
|
||||||
/// No description provided for @tryAgain.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'TRY AGAIN'**
|
|
||||||
String get tryAgain;
|
|
||||||
|
|
||||||
/// No description provided for @backToPokedex.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'BACK TO POKEDEX'**
|
|
||||||
String get backToPokedex;
|
|
||||||
|
|
||||||
/// No description provided for @system.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'SYSTEM'**
|
|
||||||
String get system;
|
|
||||||
|
|
||||||
/// No description provided for @statistics.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'STATISTICS'**
|
|
||||||
String get statistics;
|
|
||||||
|
|
||||||
/// No description provided for @colorPalette.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'COLOR PALETTE'**
|
|
||||||
String get colorPalette;
|
|
||||||
|
|
||||||
/// No description provided for @language.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'LANGUAGE'**
|
|
||||||
String get language;
|
|
||||||
|
|
||||||
/// No description provided for @statBestScore.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Best score'**
|
|
||||||
String get statBestScore;
|
|
||||||
|
|
||||||
/// No description provided for @statCaught.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Caught'**
|
|
||||||
String get statCaught;
|
|
||||||
|
|
||||||
/// No description provided for @statSeenLabel.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Seen'**
|
|
||||||
String get statSeenLabel;
|
|
||||||
|
|
||||||
/// No description provided for @statCompletion.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Completion'**
|
|
||||||
String get statCompletion;
|
|
||||||
|
|
||||||
/// No description provided for @navList.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'LIST'**
|
|
||||||
String get navList;
|
|
||||||
|
|
||||||
/// No description provided for @navGuess.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'GUESS'**
|
|
||||||
String get navGuess;
|
|
||||||
|
|
||||||
/// No description provided for @navSystem.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'SYSTEM'**
|
|
||||||
String get navSystem;
|
|
||||||
|
|
||||||
/// No description provided for @langEnglish.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'English'**
|
|
||||||
String get langEnglish;
|
|
||||||
|
|
||||||
/// No description provided for @langFrench.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'French'**
|
|
||||||
String get langFrench;
|
|
||||||
|
|
||||||
/// No description provided for @palette0.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Pokédex Red'**
|
|
||||||
String get palette0;
|
|
||||||
|
|
||||||
/// No description provided for @palette1.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Blue Ocean'**
|
|
||||||
String get palette1;
|
|
||||||
|
|
||||||
/// No description provided for @palette2.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Green Forest'**
|
|
||||||
String get palette2;
|
|
||||||
|
|
||||||
/// No description provided for @palette3.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Lightning Yellow'**
|
|
||||||
String get palette3;
|
|
||||||
|
|
||||||
/// No description provided for @palette4.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Purple Shadow'**
|
|
||||||
String get palette4;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
|
||||||
extends LocalizationsDelegate<AppLocalizations> {
|
|
||||||
const _AppLocalizationsDelegate();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<AppLocalizations> load(Locale locale) {
|
|
||||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isSupported(Locale locale) =>
|
|
||||||
<String>['en', 'fr'].contains(locale.languageCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
|
||||||
// Lookup logic when only language code is specified.
|
|
||||||
switch (locale.languageCode) {
|
|
||||||
case 'en':
|
|
||||||
return AppLocalizationsEn();
|
|
||||||
case 'fr':
|
|
||||||
return AppLocalizationsFr();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw FlutterError(
|
|
||||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
|
||||||
'an issue with the localizations generation tool. Please file an issue '
|
|
||||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
|
||||||
'that was used.');
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
// ignore: unused_import
|
|
||||||
import 'package:intl/intl.dart' as intl;
|
|
||||||
import 'app_localizations.dart';
|
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
/// The translations for English (`en`).
|
|
||||||
class AppLocalizationsEn extends AppLocalizations {
|
|
||||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get appTitle => 'Pokéguess';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get whosThatPokemon => 'WHO\'S THAT POKÉMON?';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get shinyDetected => '✨ SHINY POKÉMON DETECTED! ✨';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get genFilter => 'GEN FILTER';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get identificationInput => 'IDENTIFICATION INPUT';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get hintPrefix => 'HINT';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get enterPokemonName => 'Enter Pokémon name...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get continueButton => 'CONTINUE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get guessButton => 'GUESS!';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String hintButton(int count) {
|
|
||||||
return 'HINT ($count)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String skipButton(int count) {
|
|
||||||
return 'SKIP ($count)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get currentScore => 'CURRENT SCORE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String personalBest(int score) {
|
|
||||||
return 'PERSONAL BEST: $score';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String caughtShiny(String name) {
|
|
||||||
return '✨ SHINY! You caught $name! (+20 pts) ✨';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String caughtNormal(String name) {
|
|
||||||
return 'Correct! You caught $name!';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wrongGuess => 'Wrong guess! Try again.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get errorLoadingPokemon => 'Error loading Pokémon';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get listNational => 'LIST - NATIONAL';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tabAll => 'ALL';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tabCaught => 'CAUGHT';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get pokemonDiscovered => 'POKEMON DISCOVERED';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String noPokemonFound(String filter) {
|
|
||||||
return 'NO POKEMON FOUND IN $filter';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get pokedexFooter => 'NATIONAL POKEDEX V2.0';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get loadingError => 'Loading error';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get baseStats => 'BASE STATS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get noDescription => 'No description available for this Pokémon.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get gameOver => 'GAME OVER';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String itWas(String name) {
|
|
||||||
return 'It was $name!';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get gameOverMessage =>
|
|
||||||
'\"Looks like your journey ends here. You\'ve run out of energy!\"';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statStreak => 'STREAK';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statSeen => 'SEEN';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statScore => 'SCORE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tryAgain => 'TRY AGAIN';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backToPokedex => 'BACK TO POKEDEX';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get system => 'SYSTEM';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statistics => 'STATISTICS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get colorPalette => 'COLOR PALETTE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get language => 'LANGUAGE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statBestScore => 'Best score';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statCaught => 'Caught';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statSeenLabel => 'Seen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statCompletion => 'Completion';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navList => 'LIST';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navGuess => 'GUESS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navSystem => 'SYSTEM';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get langEnglish => 'English';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get langFrench => 'French';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette0 => 'Pokédex Red';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette1 => 'Blue Ocean';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette2 => 'Green Forest';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette3 => 'Lightning Yellow';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette4 => 'Purple Shadow';
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
// ignore: unused_import
|
|
||||||
import 'package:intl/intl.dart' as intl;
|
|
||||||
import 'app_localizations.dart';
|
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
/// The translations for French (`fr`).
|
|
||||||
class AppLocalizationsFr extends AppLocalizations {
|
|
||||||
AppLocalizationsFr([String locale = 'fr']) : super(locale);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get appTitle => 'Pokéguess';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get whosThatPokemon => 'QUI EST CE POKÉMON ?';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get shinyDetected => '✨ POKÉMON SHINY DÉTECTÉ ! ✨';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get genFilter => 'FILTRE GÉN.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get identificationInput => 'SAISIE D\'IDENTIFICATION';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get hintPrefix => 'INDICE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get enterPokemonName => 'Entrez le nom du Pokémon...';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get continueButton => 'CONTINUER';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get guessButton => 'DEVINER !';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String hintButton(int count) {
|
|
||||||
return 'INDICE ($count)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String skipButton(int count) {
|
|
||||||
return 'PASSER ($count)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get currentScore => 'SCORE ACTUEL';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String personalBest(int score) {
|
|
||||||
return 'MEILLEUR SCORE : $score';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String caughtShiny(String name) {
|
|
||||||
return '✨ SHINY ! Tu as attrapé $name ! (+20 pts) ✨';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String caughtNormal(String name) {
|
|
||||||
return 'Correct ! Tu as attrapé $name !';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wrongGuess => 'Mauvaise réponse ! Réessaie.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get errorLoadingPokemon => 'Erreur de chargement du Pokémon';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get listNational => 'LISTE - NATIONALE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tabAll => 'TOUS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tabCaught => 'ATTRAPÉS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get pokemonDiscovered => 'POKÉMON DÉCOUVERTS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String noPokemonFound(String filter) {
|
|
||||||
return 'AUCUN POKÉMON DANS $filter';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get pokedexFooter => 'POKÉDEX NATIONAL V2.0';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get loadingError => 'Erreur de chargement';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get baseStats => 'STATS DE BASE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get noDescription => 'Aucune description disponible pour ce Pokémon.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get gameOver => 'PARTIE TERMINÉE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String itWas(String name) {
|
|
||||||
return 'C\'était $name !';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get gameOverMessage =>
|
|
||||||
'« On dirait que ton aventure s\'arrête ici. Tu n\'as plus d\'énergie ! »';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statStreak => 'SÉRIE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statSeen => 'VUS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statScore => 'SCORE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tryAgain => 'REJOUER';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get backToPokedex => 'RETOUR AU POKÉDEX';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get system => 'SYSTÈME';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statistics => 'STATISTIQUES';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get colorPalette => 'PALETTE DE COULEURS';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get language => 'LANGUE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statBestScore => 'Meilleur score';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statCaught => 'Attrapés';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statSeenLabel => 'Vus';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get statCompletion => 'Complétion';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navList => 'LISTE';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navGuess => 'DEVINER';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get navSystem => 'SYSTÈME';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get langEnglish => 'Anglais';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get langFrench => 'Français';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette0 => 'Pokédex Rouge';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette1 => 'Océan Bleu';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette2 => 'Forêt Verte';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette3 => 'Foudre Jaune';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get palette4 => 'Ombre Violette';
|
|
||||||
}
|
|
||||||
@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
|
||||||
import 'presentation/pages/main_page.dart';
|
import 'presentation/pages/main_page.dart';
|
||||||
import 'presentation/pages/pokemon_detail.dart';
|
import 'presentation/pages/pokemon_detail.dart';
|
||||||
import 'presentation/pages/game_over_page.dart';
|
import 'presentation/pages/game_over_page.dart';
|
||||||
import 'presentation/providers/theme_provider.dart';
|
import 'presentation/providers/theme_provider.dart';
|
||||||
import 'presentation/providers/locale_provider.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -19,7 +17,6 @@ void main() {
|
|||||||
runApp(const ProviderScope(child: MyApp()));
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Widget racine : construit le MaterialApp (thème dérivé de la palette, routes).
|
|
||||||
class MyApp extends ConsumerWidget {
|
class MyApp extends ConsumerWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
@ -27,13 +24,9 @@ class MyApp extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final paletteIndex = ref.watch(themeProvider);
|
final paletteIndex = ref.watch(themeProvider);
|
||||||
final palette = appPalettes[paletteIndex];
|
final palette = appPalettes[paletteIndex];
|
||||||
final locale = ref.watch(localeProvider);
|
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
|
title: 'Pokéguess',
|
||||||
locale: locale,
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: palette.primary,
|
seedColor: palette.primary,
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
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 '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
|
||||||
import '../providers/repository_provider.dart';
|
import '../providers/repository_provider.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../widgets/pokemon_image.dart';
|
||||||
import '../widgets/game_over/game_over_header.dart';
|
|
||||||
import '../widgets/game_over/game_over_stats.dart';
|
|
||||||
import '../widgets/game_over/game_over_actions.dart';
|
|
||||||
import '../widgets/game_over/hinge_divider.dart';
|
|
||||||
|
|
||||||
/// Écran de fin de partie. Affiche le Pokémon manqué, les stats, et propose de rejouer.
|
|
||||||
class GameOverPage extends ConsumerStatefulWidget {
|
class GameOverPage extends ConsumerStatefulWidget {
|
||||||
const GameOverPage({Key? key}) : super(key: key);
|
const GameOverPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -39,73 +33,306 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
|
final Map<String, dynamic>? args =
|
||||||
final pokemonImage = args?['pokemonImage'] ?? '';
|
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
|
||||||
final pokemonName = args?['pokemonName'] ?? 'Unknown';
|
|
||||||
|
final String pokemonImage = args?['pokemonImage'] ?? '';
|
||||||
|
final String pokemonName = args?['pokemonName'] ?? 'Unknown';
|
||||||
final int streak = args?['streak'] ?? 0;
|
final int streak = args?['streak'] ?? 0;
|
||||||
final int score = args?['score'] ?? 0;
|
final int score = args?['score'] ?? 0;
|
||||||
|
|
||||||
|
// Pad streak with zeroes to 3 digits as in mockup (e.g. 004)
|
||||||
|
final String streakText = streak.toString().padLeft(3, '0');
|
||||||
|
|
||||||
final palette = appPalettes[ref.watch(themeProvider)];
|
// Define color palette from mockup
|
||||||
final primaryDark = HSLColor.fromColor(palette.primary)
|
const Color pokedexRed = Color(0xFFD32F2F);
|
||||||
.withLightness((HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0))
|
const Color darkRed = Color(0xFF9E1B1B);
|
||||||
.toColor();
|
const Color silverBg = Color(0xFFC8D1D8);
|
||||||
const silverBg = Color(0xFFC8D1D8);
|
const Color messageBoxBg = Color(0xFF1B2333);
|
||||||
|
const Color statBoxBg = Color(0xFFD9E0E5); // slightly lighter/different silver for stats
|
||||||
|
const Color tryAgainBtn = Color(0xFF2962FF); // Blue
|
||||||
|
const Color backBtn = Color(0xFFA66A00); // Brown
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: palette.primary,
|
backgroundColor: pokedexRed,
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
GameOverHeader(
|
// Top Box: Pokemon Silhouette & GAME OVER
|
||||||
pokemonImage: pokemonImage,
|
Container(
|
||||||
pokemonName: pokemonName,
|
decoration: BoxDecoration(
|
||||||
primaryDark: primaryDark,
|
color: darkRed, // Border color
|
||||||
surfaceColor: palette.surface,
|
border: Border.all(color: darkRed, width: 4),
|
||||||
),
|
),
|
||||||
HingeDivider(color: primaryDark),
|
child: Container(
|
||||||
Container(
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
|
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
||||||
child: Container(
|
color: silverBg,
|
||||||
width: double.infinity,
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16),
|
children: [
|
||||||
color: silverBg,
|
// GAME OVER Banner
|
||||||
child: Column(
|
Container(
|
||||||
children: [
|
color: darkRed,
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
width: double.infinity,
|
child: const Text(
|
||||||
color: palette.surface,
|
"GAME OVER",
|
||||||
padding: const EdgeInsets.all(24),
|
style: TextStyle(
|
||||||
child: Text(
|
fontSize: 26,
|
||||||
AppLocalizations.of(context)!.gameOverMessage,
|
color: Colors.yellow,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
|
letterSpacing: 2,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1.5, 1.5),
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
GameOverStats(
|
const SizedBox(height: 16),
|
||||||
streak: streak.toString().padLeft(3, '0'),
|
// Pokemon Image and Name
|
||||||
seen: "$_seenCount/${AppConstants.totalPokemon}",
|
if (pokemonImage.isNotEmpty)
|
||||||
score: "$score",
|
SizedBox(
|
||||||
|
height: 140,
|
||||||
|
child: PokemonImage(
|
||||||
|
imageUrl: pokemonImage,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
GameOverActions(
|
Text(
|
||||||
primaryColor: palette.primary,
|
"It was $pokemonName!",
|
||||||
primaryDark: primaryDark,
|
textAlign: TextAlign.center,
|
||||||
onTryAgain: () => Navigator.pop(context, true),
|
style: const TextStyle(
|
||||||
onBack: () => Navigator.pop(context, false),
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1B2333),
|
||||||
|
letterSpacing: 1,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
|
// Divider between boxes
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(height: 2, color: darkRed),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: List.generate(3, (index) =>
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: darkRed,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Container(height: 2, color: darkRed),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom Box: Message, Stats, Buttons
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: darkRed,
|
||||||
|
border: Border.all(color: darkRed, width: 4),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: silverBg,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Message Box
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
color: messageBoxBg,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: const Text(
|
||||||
|
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Stats Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: statBoxBg,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"STREAK",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
streakText,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: statBoxBg,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"SEEN",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"$_seenCount/${AppConstants.totalPokemon}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: statBoxBg,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"SCORE",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"$score",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Try Again Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh, color: Colors.white, size: 24),
|
||||||
|
label: const Text(
|
||||||
|
"TRY AGAIN",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: tryAgainBtn,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Back to Pokedex Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.menu_book, color: Colors.white, size: 24),
|
||||||
|
label: const Text(
|
||||||
|
"BACK TO POKEDEX",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: backBtn,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
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 '../../core/config/app_constants.dart';
|
||||||
import '../../domain/game/game_state.dart';
|
import '../../domain/game/game_state.dart';
|
||||||
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/pokemon_image.dart';
|
||||||
import '../widgets/scanline_overlay.dart';
|
|
||||||
import '../widgets/guess/guess_silhouette.dart';
|
|
||||||
import '../widgets/guess/gen_filter_section.dart';
|
|
||||||
import '../widgets/guess/lives_row.dart';
|
|
||||||
import '../widgets/guess/guess_input_section.dart';
|
|
||||||
import '../widgets/guess/score_board.dart';
|
|
||||||
|
|
||||||
/// Page de jeu "devine le Pokémon" : orchestre gameProvider et compose les sous-widgets.
|
|
||||||
class GuessPage extends ConsumerStatefulWidget {
|
class GuessPage extends ConsumerStatefulWidget {
|
||||||
const GuessPage({Key? key}) : super(key: key);
|
const GuessPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -29,6 +22,7 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// Démarre la partie après le premier frame (le provider est prêt).
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!_started) {
|
if (!_started) {
|
||||||
_started = true;
|
_started = true;
|
||||||
@ -46,19 +40,20 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
Future<void> _onGuess() async {
|
Future<void> _onGuess() async {
|
||||||
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
|
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final state = ref.read(gameProvider);
|
final state = ref.read(gameProvider);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case GuessResult.correct:
|
case GuessResult.correct:
|
||||||
final name = state.currentPokemon!.formatedName;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)),
|
content: Text(state.isShiny
|
||||||
|
? '✨ SHINY! You caught ${state.currentPokemon!.formatedName}! (+20 pts) ✨'
|
||||||
|
: 'Correct! You caught ${state.currentPokemon!.formatedName}!'),
|
||||||
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
|
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
case GuessResult.wrong:
|
case GuessResult.wrong:
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
content: Text(l.wrongGuess), backgroundColor: Colors.orange));
|
content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange));
|
||||||
break;
|
break;
|
||||||
case GuessResult.gameOver:
|
case GuessResult.gameOver:
|
||||||
await _showGameOver();
|
await _showGameOver();
|
||||||
@ -86,61 +81,283 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
if (playAgain == false) {
|
if (playAgain == false) {
|
||||||
ref.read(selectedTabProvider.notifier).set(0); // onglet LIST
|
ref.read(selectedTabProvider.notifier).set(0); // onglet LIST
|
||||||
}
|
}
|
||||||
// true / false / null (geste retour) relancent tous une partie pour ne pas rester bloqué.
|
// true (Try Again), false (Back to Pokédex) et null (geste retour système)
|
||||||
|
// relancent tous une nouvelle partie pour ne pas rester bloqué en game over.
|
||||||
await ref.read(gameProvider.notifier).startNewGame();
|
await ref.read(gameProvider.notifier).startNewGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _maskedName(String name) {
|
||||||
|
if (name.length <= 2) return name; // trop court pour masquer utilement
|
||||||
|
return '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
|
||||||
|
}
|
||||||
|
|
||||||
@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());
|
||||||
}
|
}
|
||||||
if (state.currentPokemon == null || state.status == GameStatus.error) {
|
if (state.currentPokemon == null || state.status == GameStatus.error) {
|
||||||
return Center(child: Text(AppLocalizations.of(context)!.errorLoadingPokemon));
|
return const Center(child: Text("Error loading Pokémon"));
|
||||||
}
|
}
|
||||||
|
|
||||||
final pokemon = state.currentPokemon!;
|
final pokemon = state.currentPokemon!;
|
||||||
final isGuessed = state.status == GameStatus.roundWon;
|
final isGuessed = state.status == GameStatus.roundWon;
|
||||||
final notifier = ref.read(gameProvider.notifier);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
const ScanlineOverlay(),
|
Positioned.fill(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 100,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) => Container(
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
color: Colors.black.withAlpha(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed, surfaceColor: palette.surface),
|
// Silhouette screen
|
||||||
GenFilterSection(
|
Container(
|
||||||
isOpen: _genFilterOpen,
|
height: 250,
|
||||||
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
|
width: double.infinity,
|
||||||
selectedGens: ref.watch(genFilterProvider),
|
margin: const EdgeInsets.all(16),
|
||||||
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
decoration: BoxDecoration(
|
||||||
surfaceColor: palette.surface,
|
color: const Color(0xFF3B6EE3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFF1B2333), width: 8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: isGuessed
|
||||||
|
? PokemonImage(
|
||||||
|
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
||||||
|
fallbackUrl: pokemon.imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
: PokemonImage(
|
||||||
|
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
||||||
|
fallbackUrl: pokemon.imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
color: state.isShiny ? Colors.yellow[700]! : Colors.black,
|
||||||
|
colorBlendMode: BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: const Color(0xFF1B2333),
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
state.isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: state.isShiny ? Colors.yellow[400] : Colors.white,
|
||||||
|
fontSize: state.isShiny ? 18 : 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Gen filter
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => setState(() => _genFilterOpen = !_genFilterOpen),
|
||||||
|
icon: Icon(
|
||||||
|
_genFilterOpen ? Icons.expand_less : Icons.filter_list,
|
||||||
|
color: const Color(0xFF1B2333),
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'GEN FILTER',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF1B2333),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: _genFilterOpen
|
||||||
|
? _GenFilterPanel(
|
||||||
|
selectedGens: ref.watch(genFilterProvider),
|
||||||
|
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LivesRow(lives: state.lives),
|
// Lives
|
||||||
const SizedBox(height: 16),
|
Row(
|
||||||
GuessInputSection(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
controller: _guessController,
|
children: List.generate(AppConstants.startingLives, (index) {
|
||||||
pokemonName: pokemon.formatedName,
|
return Icon(
|
||||||
isHintUsed: state.isHintUsed,
|
index < state.lives ? Icons.favorite : Icons.favorite_border,
|
||||||
isGuessed: isGuessed,
|
color: Colors.red,
|
||||||
hints: state.hints,
|
size: 32,
|
||||||
skips: state.skips,
|
);
|
||||||
onGuess: _onGuess,
|
}),
|
||||||
onContinue: () => notifier.loadNextPokemon(),
|
|
||||||
onHint: (state.isHintUsed || state.hints <= 0) ? null : () => notifier.useHint(),
|
|
||||||
onSkip: state.skips > 0 ? () => notifier.useSkip() : null,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
|
// Guess section
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: ScoreBoard(currentScore: state.currentScore, bestScore: state.bestScore),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("IDENTIFICATION INPUT",
|
||||||
|
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (state.isHintUsed)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber[100],
|
||||||
|
border: Border.all(color: Colors.amber[600]!, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"HINT: ${_maskedName(pokemon.formatedName)}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.grey[400]!),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: _guessController,
|
||||||
|
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'Enter Pokémon name...',
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _onGuess(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (isGuessed)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => ref.read(gameProvider.notifier).loadNextPokemon(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
child: const Text("CONTINUE",
|
||||||
|
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _onGuess,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF3B6EE3),
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
child: const Text("GUESS!",
|
||||||
|
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: (state.isHintUsed || state.hints <= 0)
|
||||||
|
? null
|
||||||
|
: () => ref.read(gameProvider.notifier).useHint(),
|
||||||
|
icon: const Icon(Icons.lightbulb, color: Colors.black87),
|
||||||
|
label: Text("HINT (${state.hints})",
|
||||||
|
style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.amber,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: state.skips > 0
|
||||||
|
? () => ref.read(gameProvider.notifier).useSkip()
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.skip_next, color: Colors.black87),
|
||||||
|
label: Text("SKIP (${state.skips})",
|
||||||
|
style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[400],
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Score
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withAlpha(153),
|
||||||
|
border: Border.all(color: Colors.black12),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text("CURRENT SCORE",
|
||||||
|
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold)),
|
||||||
|
Text("${state.currentScore}",
|
||||||
|
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3))),
|
||||||
|
const Divider(height: 24),
|
||||||
|
Text("PERSONAL BEST: ${state.bestScore}",
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
],
|
],
|
||||||
@ -151,3 +368,73 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GenFilterPanel extends StatelessWidget {
|
||||||
|
final Set<int> selectedGens;
|
||||||
|
final void Function(int) onToggle;
|
||||||
|
|
||||||
|
const _GenFilterPanel({required this.selectedGens, required this.onToggle});
|
||||||
|
|
||||||
|
static const _genNames = ['Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: const Color(0xFF1B2333), width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: List.generate(AppConstants.genRanges.length, (i) {
|
||||||
|
final range = AppConstants.genRanges[i];
|
||||||
|
final isSelected = selectedGens.contains(i);
|
||||||
|
final isLast = i == AppConstants.genRanges.length - 1;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => onToggle(i),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? const Color(0xFF1B2333).withAlpha(12) : Colors.transparent,
|
||||||
|
border: isLast ? null : Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? const Color(0xFF3B6EE3) : Colors.transparent,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? const Color(0xFF3B6EE3) : Colors.grey.shade400,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
child: isSelected ? const Icon(Icons.check, size: 13, color: Colors.white) : null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_genNames[i],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isSelected ? const Color(0xFF1B2333) : Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'#${range.$1}–#${range.$2}',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
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';
|
||||||
import 'guess_page.dart';
|
import 'guess_page.dart';
|
||||||
import 'system_page.dart';
|
import 'system_page.dart';
|
||||||
|
|
||||||
/// Coquille principale : navigation par onglets (liste / jeu / système) via un IndexedStack.
|
|
||||||
class MainPage extends ConsumerWidget {
|
class MainPage extends ConsumerWidget {
|
||||||
const MainPage({Key? key}) : super(key: key);
|
const MainPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -57,10 +55,10 @@ class MainPage extends ConsumerWidget {
|
|||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
selectedItemColor: palette.primary,
|
selectedItemColor: palette.primary,
|
||||||
unselectedItemColor: Colors.grey,
|
unselectedItemColor: Colors.grey,
|
||||||
items: [
|
items: const [
|
||||||
BottomNavigationBarItem(icon: const Icon(Icons.grid_view), label: AppLocalizations.of(context)!.navList),
|
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'LIST'),
|
||||||
BottomNavigationBarItem(icon: const Icon(Icons.games), label: AppLocalizations.of(context)!.navGuess),
|
BottomNavigationBarItem(icon: Icon(Icons.games), label: 'GUESS'),
|
||||||
BottomNavigationBarItem(icon: const Icon(Icons.settings), label: AppLocalizations.of(context)!.navSystem),
|
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'SYSTEM'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,80 +1,280 @@
|
|||||||
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 '../widgets/pokemon_type.dart';
|
||||||
import '../providers/repository_provider.dart';
|
import '../widgets/pokemon_image.dart';
|
||||||
import '../providers/theme_provider.dart';
|
|
||||||
import '../widgets/detail/pokemon_detail_top.dart';
|
|
||||||
import '../widgets/detail/pokemon_stats_panel.dart';
|
|
||||||
|
|
||||||
/// Fiche détaillée d'un Pokémon (reçu via les arguments de route).
|
class PokemonDetailPage extends StatefulWidget {
|
||||||
class PokemonDetailPage extends ConsumerStatefulWidget {
|
|
||||||
const PokemonDetailPage({Key? key}) : super(key: key);
|
const PokemonDetailPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<PokemonDetailPage> createState() => _PokemonDetailPageState();
|
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PokemonDetailPageState extends ConsumerState<PokemonDetailPage> {
|
class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
||||||
bool _isShiny = false;
|
bool _isShiny = false;
|
||||||
String? _englishGenus;
|
|
||||||
|
|
||||||
@override
|
Widget _buildStatBar(String label, int value, Color color) {
|
||||||
void didChangeDependencies() {
|
// Let's assume max base stat is 255
|
||||||
super.didChangeDependencies();
|
double ratio = (value / 255).clamp(0.0, 1.0);
|
||||||
_fetchEnglishGenusIfNeeded();
|
return Padding(
|
||||||
}
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
Future<void> _fetchEnglishGenusIfNeeded() async {
|
children: [
|
||||||
final lang = ref.read(localeProvider).languageCode;
|
SizedBox(
|
||||||
if (lang != 'en') return;
|
width: 50,
|
||||||
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
|
child: Text(
|
||||||
final genus = await ref.read(pokemonRepositoryProvider).getEnglishGenus(pokemon.id);
|
label,
|
||||||
if (mounted && genus != null) setState(() => _englishGenus = genus);
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: (ratio * 100).toInt(),
|
||||||
|
child: Container(color: color),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 100 - (ratio * 100).toInt(),
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: Text(
|
||||||
|
value.toString(),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
|
final Pokemon 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: palette.surface,
|
backgroundColor: const Color(0xFF1B2333),
|
||||||
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: palette.primary,
|
color: const Color(0xFFD32F2F),
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
border: Border.all(color: primaryDark, width: 4),
|
border: Border.all(color: const Color(0xFFA12020), width: 4),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_backBar(context, primaryDark),
|
// App Bar / Top Red Padding
|
||||||
PokemonDetailTop(
|
Container(
|
||||||
pokemon: pokemon,
|
height: 50,
|
||||||
isShiny: _isShiny,
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
|
alignment: Alignment.centerLeft,
|
||||||
primaryColor: palette.primary,
|
child: GestureDetector(
|
||||||
primaryDark: primaryDark,
|
onTap: () => Navigator.pop(context),
|
||||||
surfaceColor: palette.surface,
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFA12020),
|
||||||
|
shape: BoxShape.circle),
|
||||||
|
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// TOP SCREEN
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1B2333),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: const Color(0xFF1B2333), width: 8),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: const Color(0xFF90A4AE),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
color: const Color(0xFF1B2333),
|
||||||
|
child: Text(
|
||||||
|
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_isShiny = !_isShiny;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 180,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: const Color(0xFF81CCA5).withAlpha(153), // subtle green background behind sprite
|
||||||
|
child: PokemonImage(
|
||||||
|
imageUrl: _isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
||||||
|
fallbackUrl: _isShiny ? pokemon.imageUrl : null,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: const Color(0xFF37474F),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
pokemon.formatedName.toUpperCase(),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
PokemonTypeWidget(pokemon.type1),
|
||||||
|
if (pokemon.type2 != null) const SizedBox(width: 4),
|
||||||
|
if (pokemon.type2 != null) PokemonTypeWidget(pokemon.type2!),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// HINGE DETAILS
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
||||||
|
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_hinge(primaryDark),
|
|
||||||
const SizedBox(height: 20),
|
// BOTTOM SCREEN
|
||||||
PokemonStatsPanel(
|
Container(
|
||||||
pokemon: pokemon,
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
surfaceColor: palette.surface,
|
padding: const EdgeInsets.all(8),
|
||||||
descriptionOverride: _englishGenus,
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1B2333),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: const Color(0xFFC8D1D8),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"BASE STATS",
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"MODEL: DS-01",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(color: Colors.black38, thickness: 2, height: 20),
|
||||||
|
|
||||||
|
_buildStatBar("HP", pokemon.hp, const Color(0xFFE53935)),
|
||||||
|
_buildStatBar("ATK", pokemon.atk, const Color(0xFFFB8C00)),
|
||||||
|
_buildStatBar("DEF", pokemon.def, const Color(0xFFFDD835)),
|
||||||
|
_buildStatBar("SPD", pokemon.spd, const Color(0xFF1E88E5)),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// DESCRIPTION BOX
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE2EBF0),
|
||||||
|
border: Border.all(color: Colors.grey[400]!),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
pokemon.description != null && pokemon.description!.isNotEmpty
|
||||||
|
? '"${pokemon.description!}"'
|
||||||
|
: '"No description available for this Pokémon."',
|
||||||
|
style: const TextStyle(fontSize: 16, height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// DECORATIVE LIGHTS
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 24, height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E88E5), shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: const Color(0xFF1565C0), width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 24, height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFB300), shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: const Color(0xFFF57C00), width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
_bottomDots(primaryDark),
|
|
||||||
|
// BOTTOM DOTS
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -84,45 +284,4 @@ class _PokemonDetailPageState extends ConsumerState<PokemonDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Widget _backBar(BuildContext context, Color primaryDark) {
|
|
||||||
return Container(
|
|
||||||
height: 50,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
|
|
||||||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _hinge(Color primaryDark) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Container(height: 6, width: 40, color: primaryDark),
|
|
||||||
Container(height: 6, width: 40, color: primaryDark),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _bottomDots(Color primaryDark) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: List.generate(
|
|
||||||
4,
|
|
||||||
(i) => Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
|
||||||
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,9 @@
|
|||||||
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 '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.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/list/pokedex_list_header.dart';
|
|
||||||
import '../widgets/list/pokedex_count_bar.dart';
|
|
||||||
|
|
||||||
/// Liste du Pokédex national avec filtre ALL / CAUGHT.
|
|
||||||
class PokemonListPage extends ConsumerStatefulWidget {
|
class PokemonListPage extends ConsumerStatefulWidget {
|
||||||
const PokemonListPage({Key? key}) : super(key: key);
|
const PokemonListPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -34,8 +28,6 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext 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);
|
||||||
|
|
||||||
@ -43,31 +35,87 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const PokedexListHeader(),
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
color: const Color(0xFF90A4AE),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.menu, color: Colors.black87),
|
||||||
|
Text('LIST - NATIONAL',
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
|
Icon(Icons.search, color: Colors.black87),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Tabs
|
||||||
Container(
|
Container(
|
||||||
color: const Color(0xFF90A4AE),
|
color: const Color(0xFF90A4AE),
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab('ALL', l.tabAll, _filter == 'ALL', palette.primary),
|
_buildTab('ALL', _filter == 'ALL'),
|
||||||
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT', palette.primary),
|
_buildTab('CAUGHT', _filter == 'CAUGHT'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PokedexCountBar(caught: caughtCount, total: pokedexAsync.valueOrNull?.length ?? 0),
|
// Caught Count Bar
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFB0BEC5),
|
||||||
|
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${caughtCount.toString().padLeft(3, '0')} / ${pokedexAsync.valueOrNull?.length ?? 0}',
|
||||||
|
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Text('POKEMON DISCOVERED',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// The List
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
const ScanlineOverlay(),
|
Positioned.fill(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 100,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) => Container(
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
color: Colors.black.withAlpha(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
pokedexAsync.when(
|
pokedexAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(
|
error: (e, _) => Center(
|
||||||
child: Text('${l.loadingError}\n$e',
|
child: Text('Erreur de chargement\n$e',
|
||||||
textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.black54)),
|
||||||
),
|
),
|
||||||
data: (all) {
|
data: (all) {
|
||||||
final filtered = _applyFilter(all);
|
final filtered = _applyFilter(all);
|
||||||
if (filtered.isEmpty) return _emptyState();
|
if (filtered.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.search_off, size: 64, color: Colors.black26),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('NO POKEMON FOUND IN $_filter',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@ -79,40 +127,25 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Footer
|
||||||
Container(
|
Container(
|
||||||
height: 24,
|
height: 24,
|
||||||
color: palette.surface,
|
color: const Color(0xFF1B2333),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(l.pokedexFooter,
|
child: const Text('NATIONAL POKEDEX V2.0',
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _emptyState() {
|
Widget _buildTab(String title, bool isSelected) {
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll;
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.search_off, size: 64, color: Colors.black26),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(l.noPokemonFound(filterLabel),
|
|
||||||
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTab(String key, String label, bool isSelected, Color primaryColor) {
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (_filter != key) {
|
if (_filter != title) {
|
||||||
setState(() => _filter = key);
|
setState(() => _filter = title);
|
||||||
if (_scrollController.hasClients) _scrollController.jumpTo(0);
|
if (_scrollController.hasClients) _scrollController.jumpTo(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -120,13 +153,15 @@ 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
|
||||||
? Border(bottom: BorderSide(color: primaryColor, width: 3))
|
? const Border(bottom: BorderSide(color: Color(0xFFD32F2F), width: 3))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(label,
|
child: Text(title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isSelected ? Colors.black : Colors.black54)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,17 +2,9 @@ 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';
|
||||||
import '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
|
||||||
import '../providers/pokedex_provider.dart';
|
import '../providers/pokedex_provider.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
import '../providers/locale_provider.dart';
|
|
||||||
import '../widgets/system/system_header.dart';
|
|
||||||
import '../widgets/system/section_title.dart';
|
|
||||||
import '../widgets/system/system_stats.dart';
|
|
||||||
import '../widgets/system/palette_picker.dart';
|
|
||||||
import '../widgets/system/language_picker.dart';
|
|
||||||
|
|
||||||
/// Page "Système" : statistiques de jeu, langue et palette de couleurs.
|
|
||||||
class SystemPage extends ConsumerWidget {
|
class SystemPage extends ConsumerWidget {
|
||||||
const SystemPage({super.key});
|
const SystemPage({super.key});
|
||||||
|
|
||||||
@ -23,7 +15,6 @@ class SystemPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final paletteIndex = ref.watch(themeProvider);
|
final paletteIndex = ref.watch(themeProvider);
|
||||||
final palette = appPalettes[paletteIndex];
|
final palette = appPalettes[paletteIndex];
|
||||||
final pokedexAsync = ref.watch(pokedexProvider);
|
final pokedexAsync = ref.watch(pokedexProvider);
|
||||||
@ -32,18 +23,18 @@ class SystemPage extends ConsumerWidget {
|
|||||||
color: const Color(0xFFC8D1D8),
|
color: const Color(0xFFC8D1D8),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SystemHeader(primaryColor: palette.primary, title: l.system),
|
_Header(primaryColor: palette.primary),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SectionTitle(text: l.statistics, color: palette.primary),
|
_SectionTitle(text: 'STATISTIQUES', color: palette.primary),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
pokedexAsync.when(
|
pokedexAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (_, __) => Text(l.loadingError),
|
error: (_, __) => const Text('Erreur de chargement'),
|
||||||
data: (pokemons) {
|
data: (pokemons) {
|
||||||
final total = pokemons.length;
|
final total = pokemons.length;
|
||||||
final caught = pokemons.where((p) => p.isCaught).length;
|
final caught = pokemons.where((p) => p.isCaught).length;
|
||||||
@ -53,13 +44,13 @@ class SystemPage extends ConsumerWidget {
|
|||||||
future: _loadBestScore(),
|
future: _loadBestScore(),
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
final best = snap.data ?? 0;
|
final best = snap.data ?? 0;
|
||||||
return StatsGrid(
|
return _StatsGrid(
|
||||||
primaryColor: palette.primary,
|
primaryColor: palette.primary,
|
||||||
items: [
|
items: [
|
||||||
StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events),
|
_StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
|
||||||
StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon),
|
_StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
|
||||||
StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility),
|
_StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
|
||||||
StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart),
|
_StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -67,17 +58,9 @@ class SystemPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SectionTitle(text: l.language, color: palette.primary),
|
_SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
LanguagePicker(
|
_PalettePicker(
|
||||||
selected: ref.watch(localeProvider),
|
|
||||||
primaryColor: palette.primary,
|
|
||||||
onSelect: (loc) => ref.read(localeProvider.notifier).setLocale(loc),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
SectionTitle(text: l.colorPalette, color: palette.primary),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
PalettePicker(
|
|
||||||
selectedIndex: paletteIndex,
|
selectedIndex: paletteIndex,
|
||||||
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
|
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
|
||||||
),
|
),
|
||||||
@ -90,3 +73,188 @@ class SystemPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Header extends StatelessWidget {
|
||||||
|
final Color primaryColor;
|
||||||
|
const _Header({required this.primaryColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: primaryColor,
|
||||||
|
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'SYSTÈME',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionTitle extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
const _SectionTitle({required this.text, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 4, height: 20, color: color),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatItem {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final IconData icon;
|
||||||
|
const _StatItem({required this.label, required this.value, required this.icon});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatsGrid extends StatelessWidget {
|
||||||
|
final Color primaryColor;
|
||||||
|
final List<_StatItem> items;
|
||||||
|
const _StatsGrid({required this.primaryColor, required this.items});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.4,
|
||||||
|
children: items.map((item) => _StatCard(item: item, color: primaryColor)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatCard extends StatelessWidget {
|
||||||
|
final _StatItem item;
|
||||||
|
final Color color;
|
||||||
|
const _StatCard({required this.item, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: color.withAlpha(80), width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(item.icon, color: color, size: 28),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
item.value,
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
item.label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PalettePicker extends StatelessWidget {
|
||||||
|
final int selectedIndex;
|
||||||
|
final void Function(int) onSelect;
|
||||||
|
const _PalettePicker({required this.selectedIndex, required this.onSelect});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: List.generate(appPalettes.length, (i) {
|
||||||
|
final p = appPalettes[i];
|
||||||
|
final isSelected = i == selectedIndex;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onSelect(i),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? p.primary : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? p.primary : Colors.grey.shade300,
|
||||||
|
width: isSelected ? 3 : 1.5,
|
||||||
|
),
|
||||||
|
boxShadow: isSelected
|
||||||
|
? [BoxShadow(color: p.primary.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3))]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: p.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: p.surface,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
p.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isSelected)
|
||||||
|
const Icon(Icons.check_circle, color: Colors.white, size: 22),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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';
|
||||||
|
|
||||||
@ -18,28 +17,7 @@ class GameNotifier extends Notifier<GameState> {
|
|||||||
final _random = Random();
|
final _random = Random();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GameState build() {
|
GameState build() => const GameState();
|
||||||
// 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);
|
||||||
@ -57,9 +35,7 @@ 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 genFilter = ref.read(genFilterProvider.notifier);
|
final id = ref.read(genFilterProvider.notifier).randomId(_random);
|
||||||
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) {
|
||||||
|
|||||||
@ -3,15 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
|
|
||||||
/// Gère les générations activées pour le tirage et fournit un id aléatoire dans celles-ci.
|
|
||||||
/// La sélection est persistée dans les préférences.
|
|
||||||
class GenFilterNotifier extends Notifier<Set<int>> {
|
class GenFilterNotifier extends Notifier<Set<int>> {
|
||||||
late final Future<void> _ready;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<int> build() {
|
Set<int> build() {
|
||||||
_ready = _load();
|
_load();
|
||||||
// Default: all gens enabled — overwritten by _load() once prefs are read.
|
// Default: all gens enabled
|
||||||
return Set.from(List.generate(AppConstants.genRanges.length, (i) => i));
|
return Set.from(List.generate(AppConstants.genRanges.length, (i) => i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,9 +19,6 @@ 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)) {
|
||||||
@ -57,6 +50,5 @@ class GenFilterNotifier extends Notifier<Set<int>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensemble des index de générations actuellement activées pour le jeu.
|
|
||||||
final genFilterProvider =
|
final genFilterProvider =
|
||||||
NotifierProvider<GenFilterNotifier, Set<int>>(GenFilterNotifier.new);
|
NotifierProvider<GenFilterNotifier, Set<int>>(GenFilterNotifier.new);
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import 'dart:ui';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import '../../core/config/app_constants.dart';
|
|
||||||
|
|
||||||
/// Langues prises en charge par l'application.
|
|
||||||
const List<Locale> supportedLocales = [Locale('en'), Locale('fr')];
|
|
||||||
|
|
||||||
/// Gère la langue sélectionnée, persistée dans les préférences.
|
|
||||||
class LocaleNotifier extends Notifier<Locale> {
|
|
||||||
@override
|
|
||||||
Locale build() {
|
|
||||||
_loadSaved();
|
|
||||||
return const Locale('fr');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadSaved() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final code = prefs.getString(AppConstants.prefsLocale);
|
|
||||||
if (code != null && supportedLocales.any((l) => l.languageCode == code)) {
|
|
||||||
state = Locale(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setLocale(Locale locale) async {
|
|
||||||
state = locale;
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setString(AppConstants.prefsLocale, locale.languageCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Langue active de l'application.
|
|
||||||
final localeProvider = NotifierProvider<LocaleNotifier, Locale>(LocaleNotifier.new);
|
|
||||||
@ -5,7 +5,7 @@ import 'repository_provider.dart';
|
|||||||
/// Liste complète du Pokédex (triée par id), avec synchro initiale gérée par le repository.
|
/// 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.watch(pokemonRepositoryProvider);
|
final repo = ref.read(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,14 +4,10 @@ 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.
|
/// Point d'injection unique du repository. Sur le web, pas de SQLite (local = null).
|
||||||
/// Se reconstruit automatiquement quand la langue change,
|
|
||||||
/// ce qui invalide [pokedexProvider] en cascade.
|
|
||||||
final pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
|
final pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
|
||||||
final lang = ref.watch(localeProvider).languageCode;
|
final remote = PokemonRemoteDataSource();
|
||||||
final remote = PokemonRemoteDataSource(languageCode: lang);
|
final local = kIsWeb ? null : PokemonLocalDataSource();
|
||||||
final local = kIsWeb ? null : PokemonLocalDataSource(languageCode: lang);
|
|
||||||
return PokemonRepositoryImpl(remote: remote, local: local);
|
return PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,26 +2,28 @@ 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 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({required this.primary, required this.surface});
|
const AppPalette({
|
||||||
|
required this.name,
|
||||||
|
required this.primary,
|
||||||
|
required this.surface,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const List<AppPalette> appPalettes = [
|
const List<AppPalette> appPalettes = [
|
||||||
AppPalette(primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
|
AppPalette(name: 'Pokédex Rouge', primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
|
||||||
AppPalette(primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
|
AppPalette(name: 'Océan Bleu', primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
|
||||||
AppPalette(primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
|
AppPalette(name: 'Forêt Verte', primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
|
||||||
AppPalette(primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
|
AppPalette(name: 'Foudre Jaune', primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
|
||||||
AppPalette(primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
|
AppPalette(name: 'Ombre Violette',primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
|
||||||
];
|
];
|
||||||
|
|
||||||
const String _prefsPaletteIndex = 'palette_index';
|
const String _prefsPaletteIndex = 'palette_index';
|
||||||
|
|
||||||
/// Gère l'index de la palette sélectionnée, persistée dans les préférences.
|
|
||||||
class ThemeNotifier extends Notifier<int> {
|
class ThemeNotifier extends Notifier<int> {
|
||||||
@override
|
@override
|
||||||
int build() {
|
int build() {
|
||||||
@ -44,5 +46,4 @@ class ThemeNotifier extends Notifier<int> {
|
|||||||
AppPalette get current => appPalettes[state];
|
AppPalette get current => appPalettes[state];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index de la palette de couleurs active.
|
|
||||||
final themeProvider = NotifierProvider<ThemeNotifier, int>(ThemeNotifier.new);
|
final themeProvider = NotifierProvider<ThemeNotifier, int>(ThemeNotifier.new);
|
||||||
|
|||||||
@ -28,37 +28,8 @@ Color typeToColor(PokemonType type) {
|
|||||||
return map[type] ?? Colors.transparent;
|
return map[type] ?? Colors.transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nom du type localisé (EN par défaut, FR si languageCode == 'fr').
|
/// Nom du type avec une majuscule initiale.
|
||||||
String localizedTypeName(PokemonType type, String languageCode) {
|
String formatedTypeName(PokemonType type) {
|
||||||
if (languageCode == 'fr') {
|
final typeName = type.name;
|
||||||
const fr = {
|
return typeName[0].toUpperCase() + typeName.substring(1);
|
||||||
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');
|
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../domain/entities/pokemon.dart';
|
|
||||||
import '../pokemon_type.dart';
|
|
||||||
import '../pokemon_image.dart';
|
|
||||||
|
|
||||||
/// Écran supérieur du détail : numéro, sprite (tap pour basculer shiny), nom et types.
|
|
||||||
class PokemonDetailTop extends StatelessWidget {
|
|
||||||
final Pokemon pokemon;
|
|
||||||
final bool isShiny;
|
|
||||||
final VoidCallback onToggleShiny;
|
|
||||||
final Color primaryColor;
|
|
||||||
final Color primaryDark;
|
|
||||||
final Color surfaceColor;
|
|
||||||
|
|
||||||
const PokemonDetailTop({
|
|
||||||
super.key,
|
|
||||||
required this.pokemon,
|
|
||||||
required this.isShiny,
|
|
||||||
required this.onToggleShiny,
|
|
||||||
required this.primaryColor,
|
|
||||||
required this.primaryDark,
|
|
||||||
required this.surfaceColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: surfaceColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: surfaceColor, width: 8),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
color: const Color(0xFF90A4AE),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
color: surfaceColor,
|
|
||||||
child: Text(
|
|
||||||
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
|
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: onToggleShiny,
|
|
||||||
child: Container(
|
|
||||||
height: 180,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
color: const Color(0xFF81CCA5).withAlpha(153),
|
|
||||||
child: PokemonImage(
|
|
||||||
imageUrl: isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
|
||||||
fallbackUrl: isShiny ? pokemon.imageUrl : null,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
color: const Color(0xFF37474F),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
pokemon.formatedName.toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 2),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
PokemonTypeWidget(pokemon.type1),
|
|
||||||
if (pokemon.type2 != null) const SizedBox(width: 4),
|
|
||||||
if (pokemon.type2 != null) PokemonTypeWidget(pokemon.type2!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import '../../../domain/entities/pokemon.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
import '../../providers/locale_provider.dart';
|
|
||||||
|
|
||||||
/// Écran inférieur du détail : stats de base, description localisée et éléments décoratifs.
|
|
||||||
class PokemonStatsPanel extends ConsumerWidget {
|
|
||||||
final Pokemon pokemon;
|
|
||||||
final Color surfaceColor;
|
|
||||||
final String? descriptionOverride;
|
|
||||||
|
|
||||||
const PokemonStatsPanel({
|
|
||||||
super.key,
|
|
||||||
required this.pokemon,
|
|
||||||
required this.surfaceColor,
|
|
||||||
this.descriptionOverride,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
ref.watch(localeProvider); // watch locale so widget rebuilds on language change
|
|
||||||
final description = descriptionOverride ?? pokemon.description;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: surfaceColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
color: const Color(0xFFC8D1D8),
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(l.baseStats,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 2)),
|
|
||||||
Text("MODEL: DS-01",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(color: Colors.black38, thickness: 2, height: 20),
|
|
||||||
_StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)),
|
|
||||||
_StatBar(label: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)),
|
|
||||||
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
|
|
||||||
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE2EBF0),
|
|
||||||
border: Border.all(color: Colors.grey[400]!),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
description != null && description.isNotEmpty
|
|
||||||
? '"$description"'
|
|
||||||
: '"${l.noDescription}"',
|
|
||||||
style: const TextStyle(fontSize: 16, height: 1.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_DecorativeLights(accentColor: surfaceColor),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StatBar extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final int value;
|
|
||||||
final Color color;
|
|
||||||
const _StatBar({required this.label, required this.value, required this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ratio = (value / 255).clamp(0.0, 1.0);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 50,
|
|
||||||
child: Text(label,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
height: 14,
|
|
||||||
decoration: BoxDecoration(color: Colors.grey[400]),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: (ratio * 100).toInt(),
|
|
||||||
child: Container(color: color)),
|
|
||||||
Expanded(
|
|
||||||
flex: 100 - (ratio * 100).toInt(),
|
|
||||||
child: Container()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
SizedBox(
|
|
||||||
width: 40,
|
|
||||||
child: Text(value.toString(),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
textAlign: TextAlign.right),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DecorativeLights extends StatelessWidget {
|
|
||||||
final Color accentColor;
|
|
||||||
const _DecorativeLights({required this.accentColor});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dark = HSLColor.fromColor(accentColor)
|
|
||||||
.withLightness(
|
|
||||||
(HSLColor.fromColor(accentColor).lightness - 0.1).clamp(0.0, 1.0))
|
|
||||||
.toColor();
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: accentColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: dark, width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFFFB300),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: const Color(0xFFF57C00), width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 6,
|
|
||||||
width: 30,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[500],
|
|
||||||
borderRadius: BorderRadius.circular(3))),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Container(
|
|
||||||
height: 6,
|
|
||||||
width: 30,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[500],
|
|
||||||
borderRadius: BorderRadius.circular(3))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
|
|
||||||
class GameOverActions extends StatelessWidget {
|
|
||||||
final Color primaryColor;
|
|
||||||
final Color primaryDark;
|
|
||||||
final VoidCallback onTryAgain;
|
|
||||||
final VoidCallback onBack;
|
|
||||||
|
|
||||||
const GameOverActions({
|
|
||||||
super.key,
|
|
||||||
required this.primaryColor,
|
|
||||||
required this.primaryDark,
|
|
||||||
required this.onTryAgain,
|
|
||||||
required this.onBack,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
_button(label: l.tryAgain, icon: Icons.refresh, color: primaryColor, onPressed: onTryAgain),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_button(label: l.backToPokedex, icon: Icons.menu_book, color: primaryDark, onPressed: onBack),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _button({
|
|
||||||
required String label,
|
|
||||||
required IconData icon,
|
|
||||||
required Color color,
|
|
||||||
required VoidCallback onPressed,
|
|
||||||
}) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 60,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: Icon(icon, color: Colors.white, size: 24),
|
|
||||||
label: Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2),
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: color,
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
import '../pokemon_image.dart';
|
|
||||||
|
|
||||||
/// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon.
|
|
||||||
class GameOverHeader extends StatelessWidget {
|
|
||||||
final String pokemonImage;
|
|
||||||
final String pokemonName;
|
|
||||||
final Color primaryDark;
|
|
||||||
final Color surfaceColor;
|
|
||||||
|
|
||||||
const GameOverHeader({
|
|
||||||
super.key,
|
|
||||||
required this.pokemonImage,
|
|
||||||
required this.pokemonName,
|
|
||||||
required this.primaryDark,
|
|
||||||
required this.surfaceColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const _silverBg = Color(0xFFC8D1D8);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
|
||||||
color: _silverBg,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: primaryDark,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
l.gameOver,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 26,
|
|
||||||
color: Colors.yellow,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 2,
|
|
||||||
shadows: [Shadow(offset: Offset(1.5, 1.5), color: Colors.black)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (pokemonImage.isNotEmpty)
|
|
||||||
SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
l.itWas(pokemonName),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: surfaceColor,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE).
|
|
||||||
class GameOverStats extends StatelessWidget {
|
|
||||||
final String streak;
|
|
||||||
final String seen;
|
|
||||||
final String score;
|
|
||||||
|
|
||||||
const GameOverStats({super.key, required this.streak, required this.seen, required this.score});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _StatBox(label: l.statStreak, value: streak)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(child: _StatBox(label: l.statSeen, value: seen)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(child: _StatBox(label: l.statScore, value: score)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Une case de statistique : un libellé rouge au-dessus d'une valeur.
|
|
||||||
class _StatBox extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
const _StatBox({required this.label, required this.value});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: const Color(0xFFD9E0E5),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(color: Colors.red, fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
|
|
||||||
class HingeDivider extends StatelessWidget {
|
|
||||||
final Color color;
|
|
||||||
const HingeDivider({super.key, required this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Container(height: 2, color: color)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: List.generate(
|
|
||||||
3,
|
|
||||||
(index) => Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: Container(height: 2, color: color)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../core/config/app_constants.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations.
|
|
||||||
class GenFilterSection extends StatelessWidget {
|
|
||||||
final bool isOpen;
|
|
||||||
final VoidCallback onToggleOpen;
|
|
||||||
final Set<int> selectedGens;
|
|
||||||
final void Function(int) onToggle;
|
|
||||||
final Color surfaceColor;
|
|
||||||
|
|
||||||
const GenFilterSection({
|
|
||||||
super.key,
|
|
||||||
required this.isOpen,
|
|
||||||
required this.onToggleOpen,
|
|
||||||
required this.selectedGens,
|
|
||||||
required this.onToggle,
|
|
||||||
required this.surfaceColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: onToggleOpen,
|
|
||||||
icon: Icon(
|
|
||||||
isOpen ? Icons.expand_less : Icons.filter_list,
|
|
||||||
color: surfaceColor,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
AppLocalizations.of(context)!.genFilter,
|
|
||||||
style: TextStyle(
|
|
||||||
color: surfaceColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
letterSpacing: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: BorderSide(color: surfaceColor, width: 2),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AnimatedSize(
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
child: isOpen
|
|
||||||
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle, surfaceColor: surfaceColor)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GenFilterPanel extends StatelessWidget {
|
|
||||||
final Set<int> selectedGens;
|
|
||||||
final void Function(int) onToggle;
|
|
||||||
final Color surfaceColor;
|
|
||||||
|
|
||||||
const _GenFilterPanel({required this.selectedGens, required this.onToggle, required this.surfaceColor});
|
|
||||||
|
|
||||||
static const _genNames = [
|
|
||||||
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(top: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
border: Border.all(color: surfaceColor, width: 2),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: List.generate(AppConstants.genRanges.length, (i) {
|
|
||||||
final range = AppConstants.genRanges[i];
|
|
||||||
final isSelected = selectedGens.contains(i);
|
|
||||||
final isLast = i == AppConstants.genRanges.length - 1;
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => onToggle(i),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? const Color(0xFF1B2333).withAlpha(12) : Colors.transparent,
|
|
||||||
border: isLast ? null : Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? const Color(0xFF3B6EE3) : Colors.transparent,
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? const Color(0xFF3B6EE3) : Colors.grey.shade400,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
child: isSelected ? const Icon(Icons.check, size: 13, color: Colors.white) : null,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_genNames[i],
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
||||||
color: isSelected ? const Color(0xFF1B2333) : Colors.black54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'#${range.$1}–#${range.$2}',
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Section de saisie : indice optionnel, champ de réponse et boutons d'action
|
|
||||||
/// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks.
|
|
||||||
class GuessInputSection extends StatelessWidget {
|
|
||||||
final TextEditingController controller;
|
|
||||||
final String pokemonName;
|
|
||||||
final bool isHintUsed;
|
|
||||||
final bool isGuessed;
|
|
||||||
final int hints;
|
|
||||||
final int skips;
|
|
||||||
final VoidCallback onGuess;
|
|
||||||
final VoidCallback onContinue;
|
|
||||||
final VoidCallback? onHint;
|
|
||||||
final VoidCallback? onSkip;
|
|
||||||
|
|
||||||
const GuessInputSection({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.pokemonName,
|
|
||||||
required this.isHintUsed,
|
|
||||||
required this.isGuessed,
|
|
||||||
required this.hints,
|
|
||||||
required this.skips,
|
|
||||||
required this.onGuess,
|
|
||||||
required this.onContinue,
|
|
||||||
required this.onHint,
|
|
||||||
required this.onSkip,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Masque le nom en ne laissant visibles que la première et la dernière lettre.
|
|
||||||
String _maskedName(String name) {
|
|
||||||
if (name.length <= 2) return name; // trop court pour masquer utilement
|
|
||||||
return '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l.identificationInput,
|
|
||||||
style: const TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
if (isHintUsed)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.amber[100],
|
|
||||||
border: Border.all(color: Colors.amber[600]!, width: 2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"${l.hintPrefix}: ${_maskedName(pokemonName)}",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.grey[400]!)),
|
|
||||||
child: TextField(
|
|
||||||
controller: controller,
|
|
||||||
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: l.enterPokemonName,
|
|
||||||
),
|
|
||||||
onSubmitted: (_) => onGuess(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (isGuessed)
|
|
||||||
_bigButton(label: l.continueButton, color: Colors.green, onPressed: onContinue)
|
|
||||||
else ...[
|
|
||||||
_bigButton(label: l.guessButton, color: const Color(0xFF3B6EE3), onPressed: onGuess),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _actionButton(icon: Icons.lightbulb, label: l.hintButton(hints), color: Colors.amber, onPressed: onHint),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _actionButton(icon: Icons.skip_next, label: l.skipButton(skips), color: Colors.grey[400]!, onPressed: onSkip),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _bigButton({required String label, required Color color, required VoidCallback onPressed}) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 60,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: color,
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _actionButton({
|
|
||||||
required IconData icon,
|
|
||||||
required String label,
|
|
||||||
required Color color,
|
|
||||||
required VoidCallback? onPressed,
|
|
||||||
}) {
|
|
||||||
return ElevatedButton.icon(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: Icon(icon, color: Colors.black87),
|
|
||||||
label: Text(label, style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: color,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../domain/entities/pokemon.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
import '../pokemon_image.dart';
|
|
||||||
|
|
||||||
/// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée).
|
|
||||||
class GuessSilhouette extends StatelessWidget {
|
|
||||||
final Pokemon pokemon;
|
|
||||||
final bool isShiny;
|
|
||||||
final bool isGuessed;
|
|
||||||
final Color surfaceColor;
|
|
||||||
|
|
||||||
const GuessSilhouette({
|
|
||||||
super.key,
|
|
||||||
required this.pokemon,
|
|
||||||
required this.isShiny,
|
|
||||||
required this.isGuessed,
|
|
||||||
required this.surfaceColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl;
|
|
||||||
return Container(
|
|
||||||
height: 250,
|
|
||||||
width: double.infinity,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF3B6EE3),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: surfaceColor, width: 8),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: isGuessed
|
|
||||||
? PokemonImage(
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
fallbackUrl: pokemon.imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
)
|
|
||||||
: PokemonImage(
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
fallbackUrl: pokemon.imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
color: isShiny ? Colors.yellow[700]! : Colors.black,
|
|
||||||
colorBlendMode: BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
color: surfaceColor,
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
isShiny ? l.shinyDetected : l.whosThatPokemon,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: isShiny ? Colors.yellow[400] : Colors.white,
|
|
||||||
fontSize: isShiny ? 18 : 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../core/config/app_constants.dart';
|
|
||||||
|
|
||||||
/// Rangée de cœurs représentant les vies restantes.
|
|
||||||
class LivesRow extends StatelessWidget {
|
|
||||||
final int lives;
|
|
||||||
const LivesRow({super.key, required this.lives});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: List.generate(AppConstants.startingLives, (index) {
|
|
||||||
return Icon(
|
|
||||||
index < lives ? Icons.favorite : Icons.favorite_border,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 32,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Encart affichant le score courant et le meilleur score personnel.
|
|
||||||
class ScoreBoard extends StatelessWidget {
|
|
||||||
final int currentScore;
|
|
||||||
final int bestScore;
|
|
||||||
|
|
||||||
const ScoreBoard({super.key, required this.currentScore, required this.bestScore});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withAlpha(153),
|
|
||||||
border: Border.all(color: Colors.black12),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l.currentScore,
|
|
||||||
style: const TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"$currentScore",
|
|
||||||
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
|
|
||||||
),
|
|
||||||
const Divider(height: 24),
|
|
||||||
Text(
|
|
||||||
l.personalBest(bestScore),
|
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Bandeau affichant le nombre de Pokémon découverts sur le total.
|
|
||||||
class PokedexCountBar extends StatelessWidget {
|
|
||||||
final int caught;
|
|
||||||
final int total;
|
|
||||||
const PokedexCountBar({super.key, required this.caught, required this.total});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFFB0BEC5),
|
|
||||||
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'${caught.toString().padLeft(3, '0')} / $total',
|
|
||||||
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
Text(AppLocalizations.of(context)!.pokemonDiscovered,
|
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// Barre de titre de la liste du Pokédex.
|
|
||||||
class PokedexListHeader extends StatelessWidget {
|
|
||||||
const PokedexListHeader({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
color: const Color(0xFF90A4AE),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(AppLocalizations.of(context)!.listNational,
|
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Image réseau d'un Pokémon, avec URL de repli et placeholder en cas d'échec de chargement.
|
|
||||||
class PokemonImage extends StatelessWidget {
|
class PokemonImage extends StatelessWidget {
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String? fallbackUrl;
|
final String? fallbackUrl;
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import 'pokemon_image.dart';
|
import 'pokemon_image.dart';
|
||||||
|
|
||||||
/// Ligne de liste d'un Pokémon : image et nom si attrapé, masqué (???) sinon.
|
|
||||||
class PokemonTile extends StatelessWidget {
|
class PokemonTile extends StatelessWidget {
|
||||||
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
|
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import '../theme/type_colors.dart';
|
import '../theme/type_colors.dart';
|
||||||
|
|
||||||
/// Pastille colorée affichant le nom d'un type de Pokémon (couleur de fond selon le type).
|
// Widget qui permet d'afficher un type de Pokémon
|
||||||
|
// Elle prend en paramètre un type de Pokémon
|
||||||
|
// Elle affiche le nom du type avec une couleur de fond correspondant au type
|
||||||
class PokemonTypeWidget extends StatelessWidget {
|
class PokemonTypeWidget extends StatelessWidget {
|
||||||
const PokemonTypeWidget(this.type, {Key? key}) : super(key: key);
|
const PokemonTypeWidget(this.type, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -10,8 +12,7 @@ class PokemonTypeWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final lang = Localizations.localeOf(context).languageCode;
|
String typeName = formatedTypeName(type);
|
||||||
String typeName = localizedTypeName(type, lang);
|
|
||||||
Color typeColor = typeToColor(type);
|
Color typeColor = typeToColor(type);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Effet rétro de scanlines superposé en fond. À placer directement dans un [Stack].
|
|
||||||
class ScanlineOverlay extends StatelessWidget {
|
|
||||||
const ScanlineOverlay({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Positioned.fill(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: 100,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemBuilder: (context, index) => Container(
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
|
||||||
color: Colors.black.withAlpha(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
import '../../providers/locale_provider.dart';
|
|
||||||
|
|
||||||
/// Sélecteur de langue de l'application (parmi [supportedLocales]).
|
|
||||||
class LanguagePicker extends StatelessWidget {
|
|
||||||
final Locale selected;
|
|
||||||
final Color primaryColor;
|
|
||||||
final void Function(Locale) onSelect;
|
|
||||||
|
|
||||||
const LanguagePicker({
|
|
||||||
super.key,
|
|
||||||
required this.selected,
|
|
||||||
required this.primaryColor,
|
|
||||||
required this.onSelect,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final names = {'en': l.langEnglish, 'fr': l.langFrench};
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: supportedLocales.map((locale) {
|
|
||||||
final isSelected = locale.languageCode == selected.languageCode;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => onSelect(locale),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? primaryColor : Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? primaryColor : Colors.grey.shade300,
|
|
||||||
width: isSelected ? 3 : 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Text(
|
|
||||||
names[locale.languageCode] ?? locale.languageCode,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isSelected ? Colors.white : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
import '../../providers/theme_provider.dart';
|
|
||||||
|
|
||||||
/// Sélecteur de palette de couleurs de l'application.
|
|
||||||
class PalettePicker extends StatelessWidget {
|
|
||||||
final int selectedIndex;
|
|
||||||
final void Function(int) onSelect;
|
|
||||||
const PalettePicker({super.key, required this.selectedIndex, required this.onSelect});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context)!;
|
|
||||||
final paletteNames = [l.palette0, l.palette1, l.palette2, l.palette3, l.palette4];
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: List.generate(appPalettes.length, (i) {
|
|
||||||
final p = appPalettes[i];
|
|
||||||
final isSelected = i == selectedIndex;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => onSelect(i),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? p.primary : Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? p.primary : Colors.grey.shade300,
|
|
||||||
width: isSelected ? 3 : 1.5,
|
|
||||||
),
|
|
||||||
boxShadow: isSelected
|
|
||||||
? [BoxShadow(color: p.primary.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3))]
|
|
||||||
: [],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: p.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: p.surface,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 1.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Text(
|
|
||||||
paletteNames[i],
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isSelected ? Colors.white : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Titre de section avec une barre verticale colorée à gauche.
|
|
||||||
class SectionTitle extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
final Color color;
|
|
||||||
const SectionTitle({super.key, required this.text, required this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Container(width: 4, height: 20, color: color),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color, letterSpacing: 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// En-tête de la page Système.
|
|
||||||
class SystemHeader extends StatelessWidget {
|
|
||||||
final Color primaryColor;
|
|
||||||
final String title;
|
|
||||||
const SystemHeader({super.key, required this.primaryColor, required this.title});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: primaryColor,
|
|
||||||
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Donnée d'une statistique affichée dans la grille.
|
|
||||||
class StatItem {
|
|
||||||
final String label;
|
|
||||||
final String value;
|
|
||||||
final IconData icon;
|
|
||||||
const StatItem({required this.label, required this.value, required this.icon});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Grille 2 colonnes de cartes de statistiques.
|
|
||||||
class StatsGrid extends StatelessWidget {
|
|
||||||
final Color primaryColor;
|
|
||||||
final List<StatItem> items;
|
|
||||||
const StatsGrid({super.key, required this.primaryColor, required this.items});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GridView.count(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
childAspectRatio: 1.4,
|
|
||||||
children: items.map((item) => _StatCard(item: item, color: primaryColor)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StatCard extends StatelessWidget {
|
|
||||||
final StatItem item;
|
|
||||||
final Color color;
|
|
||||||
const _StatCard({required this.item, required this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: color.withAlpha(80), width: 2),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, 2)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(item.icon, color: color, size: 28),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(item.value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color)),
|
|
||||||
Text(
|
|
||||||
item.label,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ 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)
|
||||||
|
|||||||
@ -11,11 +11,6 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# Internationalisation (FR / EN)
|
|
||||||
flutter_localizations:
|
|
||||||
sdk: flutter
|
|
||||||
intl: any
|
|
||||||
|
|
||||||
# Shared API
|
# Shared API
|
||||||
sqflite_common: ^2.5.0
|
sqflite_common: ^2.5.0
|
||||||
|
|
||||||
@ -40,4 +35,3 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
generate: true # active la génération des localisations (gen-l10n)
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user