Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a41bd50a6 | ||
|
|
a2a7ffd79f | ||
|
|
409ab9c1dd | ||
|
|
d13f8d8834 | ||
|
|
afacacaa10 | ||
|
|
7fd122fd91 | ||
|
|
9d49cef271 | ||
|
|
e93384bf71 | ||
|
|
b12739af09 | ||
|
|
749314d312 | ||
|
|
dd33885bda | ||
|
|
41a80f03f0 | ||
|
|
93e3fa76c8 | ||
|
|
bdbd2fcc6a | ||
|
|
2f0d7e1e36 | ||
|
|
cb9e214aa2 | ||
|
|
d12b01b4ab | ||
|
|
1b9618de84 |
@@ -1,37 +1,87 @@
|
|||||||
# pokedex
|
# Pokéguess
|
||||||
|
|
||||||
Projet d'exemple d'application Flutter pour les cours de bootcamp Ynov 2023.
|
> Devine le Pokémon à partir de sa silhouette !
|
||||||
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://github.com/Efilown/pokedex-example.git
|
git clone https://gitea.losvernos.com/guillaumev/quel-est-ce-pokemon.git
|
||||||
cd pokedex-example
|
cd quel-est-ce-pokemon
|
||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
Puis lancer l'application sur le device de votre choix (téléphone, émulateur ou
|
||||||
|
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
|
||||||
|
|
||||||
Pour les images (remplacer par l'id du pokémon désiré):
|
- [API Tyradex](https://tyradex.app) — données des Pokémon (français)
|
||||||
|
- 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)
|
## Pistes d'amélioration
|
||||||
- [Sprite shiny](https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/6.png)
|
|
||||||
|
|
||||||
Autres liens
|
- Lecteur audio pour écouter le cri du Pokémon
|
||||||
|
- Internationalisation (FR / EN)
|
||||||
- [l'API pour récupérer les informations](https://pokeapi.co/api/v2/pokemon/6)
|
- Vue « mon équipe » (6 Pokémon choisis)
|
||||||
- [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 !
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 775 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_en.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
|
output-dir: lib/l10n
|
||||||
@@ -41,6 +41,9 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"@@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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"@@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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// 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';
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// 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';
|
||||||
|
}
|
||||||
+8
-1
@@ -3,10 +3,12 @@ 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();
|
||||||
@@ -17,6 +19,7 @@ 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});
|
||||||
|
|
||||||
@@ -24,9 +27,13 @@ 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(
|
||||||
title: 'Pokéguess',
|
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
|
||||||
|
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,9 +1,14 @@
|
|||||||
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 '../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);
|
||||||
|
|
||||||
@@ -33,28 +38,18 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Map<String, dynamic>? args =
|
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
|
final pokemonImage = args?['pokemonImage'] ?? '';
|
||||||
|
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)
|
const darkRed = Color(0xFF9E1B1B);
|
||||||
final String streakText = streak.toString().padLeft(3, '0');
|
const silverBg = Color(0xFFC8D1D8);
|
||||||
|
const messageBoxBg = Color(0xFF1B2333);
|
||||||
// Define color palette from mockup
|
|
||||||
const Color pokedexRed = Color(0xFFD32F2F);
|
|
||||||
const Color darkRed = Color(0xFF9E1B1B);
|
|
||||||
const Color 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: pokedexRed,
|
backgroundColor: const Color(0xFFD32F2F),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
@@ -62,268 +57,36 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Top Box: Pokemon Silhouette & GAME OVER
|
GameOverHeader(pokemonImage: pokemonImage, pokemonName: pokemonName),
|
||||||
|
const HingeDivider(),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: darkRed, border: Border.all(color: darkRed, width: 4)),
|
||||||
color: darkRed, // Border color
|
|
||||||
border: Border.all(color: darkRed, width: 4),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
|
||||||
color: silverBg,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// GAME OVER Banner
|
|
||||||
Container(
|
|
||||||
color: darkRed,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
|
||||||
child: const Text(
|
|
||||||
"GAME OVER",
|
|
||||||
style: 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),
|
|
||||||
// Pokemon Image and Name
|
|
||||||
if (pokemonImage.isNotEmpty)
|
|
||||||
SizedBox(
|
|
||||||
height: 140,
|
|
||||||
child: PokemonImage(
|
|
||||||
imageUrl: pokemonImage,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
"It was $pokemonName!",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
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(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
color: silverBg,
|
color: silverBg,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Message Box
|
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: messageBoxBg,
|
color: messageBoxBg,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: const Text(
|
child: Text(
|
||||||
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
|
AppLocalizations.of(context)!.gameOverMessage,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: const TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
GameOverStats(
|
||||||
// Stats Row
|
streak: streak.toString().padLeft(3, '0'),
|
||||||
Row(
|
seen: "$_seenCount/${AppConstants.totalPokemon}",
|
||||||
children: [
|
score: "$score",
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
|
GameOverActions(
|
||||||
// Try Again Button
|
onTryAgain: () => Navigator.pop(context, true),
|
||||||
SizedBox(
|
onBack: () => Navigator.pop(context, false),
|
||||||
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,12 +1,18 @@
|
|||||||
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 '../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);
|
||||||
|
|
||||||
@@ -22,7 +28,6 @@ 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;
|
||||||
@@ -40,20 +45,19 @@ 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
|
content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)),
|
||||||
? '✨ 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(const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange));
|
content: Text(l.wrongGuess), backgroundColor: Colors.orange));
|
||||||
break;
|
break;
|
||||||
case GuessResult.gameOver:
|
case GuessResult.gameOver:
|
||||||
await _showGameOver();
|
await _showGameOver();
|
||||||
@@ -81,16 +85,10 @@ 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 (Try Again), false (Back to Pokédex) et null (geste retour système)
|
// true / false / null (geste retour) relancent tous une partie pour ne pas rester bloqué.
|
||||||
// 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);
|
||||||
@@ -99,265 +97,47 @@ class _GuessPageState extends ConsumerState<GuessPage> {
|
|||||||
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 const Center(child: Text("Error loading Pokémon"));
|
return Center(child: Text(AppLocalizations.of(context)!.errorLoadingPokemon));
|
||||||
}
|
}
|
||||||
|
|
||||||
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: [
|
||||||
Positioned.fill(
|
const ScanlineOverlay(),
|
||||||
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: [
|
||||||
// Silhouette screen
|
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed),
|
||||||
Container(
|
GenFilterSection(
|
||||||
height: 250,
|
isOpen: _genFilterOpen,
|
||||||
width: double.infinity,
|
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
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),
|
selectedGens: ref.watch(genFilterProvider),
|
||||||
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Lives
|
LivesRow(lives: state.lives),
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: List.generate(AppConstants.startingLives, (index) {
|
|
||||||
return Icon(
|
|
||||||
index < state.lives ? Icons.favorite : Icons.favorite_border,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 32,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Guess section
|
GuessInputSection(
|
||||||
|
controller: _guessController,
|
||||||
|
pokemonName: pokemon.formatedName,
|
||||||
|
isHintUsed: state.isHintUsed,
|
||||||
|
isGuessed: isGuessed,
|
||||||
|
hints: state.hints,
|
||||||
|
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),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: Column(
|
child: ScoreBoard(currentScore: state.currentScore, bestScore: state.bestScore),
|
||||||
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),
|
||||||
],
|
],
|
||||||
@@ -368,73 +148,3 @@ 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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../domain/entities/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import '../widgets/pokemon_type.dart';
|
import '../widgets/detail/pokemon_detail_top.dart';
|
||||||
import '../widgets/pokemon_image.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 StatefulWidget {
|
||||||
const PokemonDetailPage({Key? key}) : super(key: key);
|
const PokemonDetailPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -13,57 +14,9 @@ class PokemonDetailPage extends StatefulWidget {
|
|||||||
class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
||||||
bool _isShiny = false;
|
bool _isShiny = false;
|
||||||
|
|
||||||
Widget _buildStatBar(String label, int value, Color color) {
|
|
||||||
// Let's assume max base stat is 255
|
|
||||||
double 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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
|
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF1B2333),
|
backgroundColor: const Color(0xFF1B2333),
|
||||||
@@ -79,202 +32,18 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// App Bar / Top Red Padding
|
_backBar(context),
|
||||||
Container(
|
PokemonDetailTop(
|
||||||
height: 50,
|
pokemon: pokemon,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
isShiny: _isShiny,
|
||||||
alignment: Alignment.centerLeft,
|
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
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(),
|
||||||
// BOTTOM SCREEN
|
const SizedBox(height: 20),
|
||||||
Container(
|
PokemonStatsPanel(pokemon: pokemon),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
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(),
|
||||||
// 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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -284,4 +53,45 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _backBar(BuildContext context) {
|
||||||
|
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: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
|
||||||
|
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _hinge() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
||||||
|
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _bottomDots() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(
|
||||||
|
4,
|
||||||
|
(i) => Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
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 '../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);
|
||||||
|
|
||||||
@@ -28,6 +33,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final pokedexAsync = ref.watch(pokedexProvider);
|
final pokedexAsync = ref.watch(pokedexProvider);
|
||||||
final caughtCount = ref.watch(caughtCountProvider);
|
final caughtCount = ref.watch(caughtCountProvider);
|
||||||
|
|
||||||
@@ -35,87 +41,31 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
const PokedexListHeader(),
|
||||||
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', _filter == 'ALL'),
|
_buildTab('ALL', l.tabAll, _filter == 'ALL'),
|
||||||
_buildTab('CAUGHT', _filter == 'CAUGHT'),
|
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Caught Count Bar
|
PokedexCountBar(caught: caughtCount, total: pokedexAsync.valueOrNull?.length ?? 0),
|
||||||
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: [
|
||||||
Positioned.fill(
|
const ScanlineOverlay(),
|
||||||
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('Erreur de chargement\n$e',
|
child: Text('${l.loadingError}\n$e',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
|
||||||
style: const TextStyle(color: Colors.black54)),
|
|
||||||
),
|
),
|
||||||
data: (all) {
|
data: (all) {
|
||||||
final filtered = _applyFilter(all);
|
final filtered = _applyFilter(all);
|
||||||
if (filtered.isEmpty) {
|
if (filtered.isEmpty) return _emptyState();
|
||||||
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),
|
||||||
@@ -127,25 +77,40 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Footer
|
|
||||||
Container(
|
Container(
|
||||||
height: 24,
|
height: 24,
|
||||||
color: const Color(0xFF1B2333),
|
color: const Color(0xFF1B2333),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text('NATIONAL POKEDEX V2.0',
|
child: Text(l.pokedexFooter,
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTab(String title, bool isSelected) {
|
Widget _emptyState() {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll;
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.search_off, size: 64, color: Colors.black26),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(l.noPokemonFound(filterLabel),
|
||||||
|
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTab(String key, String label, bool isSelected) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (_filter != title) {
|
if (_filter != key) {
|
||||||
setState(() => _filter = title);
|
setState(() => _filter = key);
|
||||||
if (_scrollController.hasClients) _scrollController.jumpTo(0);
|
if (_scrollController.hasClients) _scrollController.jumpTo(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -157,11 +122,9 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(title,
|
child: Text(label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: isSelected ? Colors.black : Colors.black54)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ 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});
|
||||||
|
|
||||||
@@ -15,6 +23,7 @@ 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);
|
||||||
@@ -23,18 +32,18 @@ class SystemPage extends ConsumerWidget {
|
|||||||
color: const Color(0xFFC8D1D8),
|
color: const Color(0xFFC8D1D8),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_Header(primaryColor: palette.primary),
|
SystemHeader(primaryColor: palette.primary, title: l.system),
|
||||||
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: 'STATISTIQUES', color: palette.primary),
|
SectionTitle(text: l.statistics, 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: (_, __) => const Text('Erreur de chargement'),
|
error: (_, __) => Text(l.loadingError),
|
||||||
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;
|
||||||
@@ -44,13 +53,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: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
|
StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events),
|
||||||
_StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
|
StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon),
|
||||||
_StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
|
StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility),
|
||||||
_StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
|
StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -58,9 +67,17 @@ class SystemPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
|
SectionTitle(text: l.language, color: palette.primary),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_PalettePicker(
|
LanguagePicker(
|
||||||
|
selected: ref.watch(localeProvider),
|
||||||
|
primaryColor: palette.primary,
|
||||||
|
onSelect: (loc) => ref.read(localeProvider.notifier).setLocale(loc),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SectionTitle(text: l.colorPalette, color: palette.primary),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
PalettePicker(
|
||||||
selectedIndex: paletteIndex,
|
selectedIndex: paletteIndex,
|
||||||
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
|
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
|
||||||
),
|
),
|
||||||
@@ -73,188 +90,3 @@ 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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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>> {
|
||||||
@override
|
@override
|
||||||
Set<int> build() {
|
Set<int> build() {
|
||||||
@@ -50,5 +52,6 @@ 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);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
|
||||||
|
/// Langues prises en charge par l'application.
|
||||||
|
const List<Locale> supportedLocales = [Locale('en'), Locale('fr')];
|
||||||
|
|
||||||
|
/// Gère la langue sélectionnée, persistée dans les préférences.
|
||||||
|
class LocaleNotifier extends Notifier<Locale> {
|
||||||
|
@override
|
||||||
|
Locale build() {
|
||||||
|
_loadSaved();
|
||||||
|
return const Locale('en');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Palette de couleurs nommée de l'application (couleur primaire + surface de fond).
|
||||||
class AppPalette {
|
class AppPalette {
|
||||||
final String name;
|
final String name;
|
||||||
final Color primary;
|
final Color primary;
|
||||||
@@ -24,6 +25,7 @@ const List<AppPalette> appPalettes = [
|
|||||||
|
|
||||||
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() {
|
||||||
@@ -46,4 +48,5 @@ 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);
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const PokemonDetailTop({
|
||||||
|
super.key,
|
||||||
|
required this.pokemon,
|
||||||
|
required this.isShiny,
|
||||||
|
required this.onToggleShiny,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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: 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!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../domain/entities/pokemon.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
/// Écran inférieur du détail : stats de base, description et éléments décoratifs.
|
||||||
|
class PokemonStatsPanel extends StatelessWidget {
|
||||||
|
final Pokemon pokemon;
|
||||||
|
const PokemonStatsPanel({super.key, required this.pokemon});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
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: [
|
||||||
|
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(
|
||||||
|
pokemon.description != null && pokemon.description!.isNotEmpty
|
||||||
|
? '"${pokemon.description!}"'
|
||||||
|
: '"${l.noDescription}"',
|
||||||
|
style: const TextStyle(fontSize: 16, height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const _DecorativeLights(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Barre d'une statistique (libellé, jauge proportionnelle, valeur).
|
||||||
|
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); // stat de base max supposée = 255
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Petites diodes décoratives en bas du panneau (purement cosmétiques).
|
||||||
|
class _DecorativeLights extends StatelessWidget {
|
||||||
|
const _DecorativeLights();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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 VoidCallback onTryAgain;
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
const GameOverActions({super.key, 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: const Color(0xFF2962FF), onPressed: onTryAgain),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_button(label: l.backToPokedex, icon: Icons.menu_book, color: const Color(0xFFA66A00), 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const GameOverHeader({super.key, required this.pokemonImage, required this.pokemonName});
|
||||||
|
|
||||||
|
static const _darkRed = Color(0xFF9E1B1B);
|
||||||
|
static const _silverBg = Color(0xFFC8D1D8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
||||||
|
color: _silverBg,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: _darkRed,
|
||||||
|
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: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1B2333),
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
|
||||||
|
class HingeDivider extends StatelessWidget {
|
||||||
|
const HingeDivider({super.key});
|
||||||
|
|
||||||
|
static const _darkRed = Color(0xFF9E1B1B);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const GenFilterSection({
|
||||||
|
super.key,
|
||||||
|
required this.isOpen,
|
||||||
|
required this.onToggleOpen,
|
||||||
|
required this.selectedGens,
|
||||||
|
required this.onToggle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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: const Color(0xFF1B2333),
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
AppLocalizations.of(context)!.genFilter,
|
||||||
|
style: const 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: isOpen
|
||||||
|
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const GuessSilhouette({
|
||||||
|
super.key,
|
||||||
|
required this.pokemon,
|
||||||
|
required this.isShiny,
|
||||||
|
required this.isGuessed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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: const Color(0xFF1B2333), 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: const Color(0xFF1B2333),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.menu, color: Colors.black87),
|
||||||
|
Text(AppLocalizations.of(context)!.listNational,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
|
const Icon(Icons.search, color: Colors.black87),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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,6 +2,7 @@ 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,9 +2,7 @@ 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';
|
||||||
|
|
||||||
// Widget qui permet d'afficher un type de Pokémon
|
/// Pastille colorée affichant le nom d'un type de Pokémon (couleur de fond selon le type).
|
||||||
// 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);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.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 _names = {'en': 'English', 'fr': 'Français'};
|
||||||
|
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter/material.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) {
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@ 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
|
||||||
|
|
||||||
@@ -35,3 +40,4 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
generate: true # active la génération des localisations (gen-l10n)
|
||||||
|
|||||||
Reference in New Issue
Block a user