Compare commits

..
Author SHA1 Message Date
maxiwere45 409ab9c1dd Merge: update README for current project state 2026-06-23 12:00:50 +02:00
maxiwere45andClaude Opus 4.8 d13f8d8834 docs: rewrite README to reflect Pokéguess (game, architecture, stack)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:00:50 +02:00
maxiwere45 afacacaa10 Merge: document all public classes (code documentation) 2026-06-23 11:58:29 +02:00
maxiwere45andClaude Opus 4.8 7fd122fd91 docs(code): add doc comments to all public widgets and providers
Document MyApp, MainPage, PokemonImage/Tile/TypeWidget, and the gen-filter
and theme providers/notifiers. Every public class/enum now has a /// doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:58:29 +02:00
maxiwere45 9d49cef271 Merge: split pokemon_list into sub-widgets (<150 lines) 2026-06-23 11:53:27 +02:00
maxiwere45andClaude Opus 4.8 e93384bf71 refactor(list): split pokemon_list into sub-widgets (<150 lines)
Extract PokedexListHeader, PokedexCountBar; reuse shared ScanlineOverlay.
pokemon_list.dart 169 -> 128 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:53:27 +02:00
maxiwere45 b12739af09 Merge: split system_page into sub-widgets (<150 lines) 2026-06-23 11:51:15 +02:00
maxiwere45andClaude Opus 4.8 749314d312 refactor(system): extract system_page sub-widgets into separate files (<150 lines)
Move SystemHeader, SectionTitle, StatsGrid/StatItem and PalettePicker into
widgets/system/. system_page.dart 260 -> 80 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:51:15 +02:00
maxiwere45 dd33885bda Merge: split pokemon_detail into sub-widgets (<150 lines) 2026-06-23 11:49:19 +02:00
maxiwere45andClaude Opus 4.8 41a80f03f0 refactor(detail): split pokemon_detail into sub-widgets (<150 lines)
Extract PokemonDetailTop and PokemonStatsPanel (+ reusable _StatBar).
pokemon_detail.dart 286 -> 97 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:49:19 +02:00
maxiwere45 93e3fa76c8 Merge: split game_over_page into sub-widgets (<150 lines) 2026-06-23 11:47:08 +02:00
maxiwere45andClaude Opus 4.8 bdbd2fcc6a refactor(game-over): split game_over_page into sub-widgets (<150 lines)
Extract GameOverHeader, GameOverStats (+ reusable _StatBox), HingeDivider,
GameOverActions. game_over_page.dart 338 -> 100 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:47:08 +02:00
maxiwere45 2f0d7e1e36 Merge: split guess_page into sub-widgets (<150 lines) 2026-06-23 11:44:55 +02:00
maxiwere45andClaude Opus 4.8 cb9e214aa2 refactor(guess): split guess_page into focused sub-widgets (<150 lines)
Extract GuessSilhouette, GenFilterSection, LivesRow, GuessInputSection,
ScoreBoard and a shared ScanlineOverlay. guess_page.dart 440 -> 149 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:44:55 +02:00
maxiwere45 d12b01b4ab chore: add documents 2026-06-23 11:36:13 +02:00
maxiwere45 1b9618de84 Merge branch 'pokeguess-preview' 2026-06-23 11:34:23 +02:00
34 changed files with 1316 additions and 1142 deletions
+74 -24
View File
@@ -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

+1
View File
@@ -17,6 +17,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});
+50 -288
View File
@@ -2,8 +2,12 @@ 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 '../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,306 +37,64 @@ 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(
child: Padding( child: Padding(
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),
Container( const HingeDivider(),
decoration: BoxDecoration( Container(
color: darkRed, // Border color decoration: BoxDecoration(color: darkRed, border: Border.all(color: darkRed, width: 4)),
border: Border.all(color: darkRed, width: 4), child: Container(
), width: double.infinity,
child: Container( padding: const EdgeInsets.all(16),
width: double.infinity, color: silverBg,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), child: Column(
color: silverBg, children: [
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( Container(
width: 8, width: double.infinity,
height: 8, color: messageBoxBg,
margin: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.all(24),
decoration: const BoxDecoration( child: const Text(
color: darkRed, "\"Looks like your journey\nends here. You've run out\nof energy!\"",
shape: BoxShape.circle, textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
), ),
) ),
), const SizedBox(height: 16),
GameOverStats(
streak: streak.toString().padLeft(3, '0'),
seen: "$_seenCount/${AppConstants.totalPokemon}",
score: "$score",
),
const SizedBox(height: 16),
GameOverActions(
onTryAgain: () => Navigator.pop(context, true),
onBack: () => Navigator.pop(context, false),
),
],
), ),
const SizedBox(width: 8),
Expanded(
child: Container(height: 2, color: darkRed),
),
],
),
),
// Bottom Box: Message, Stats, Buttons
Container(
decoration: BoxDecoration(
color: darkRed,
border: Border.all(color: darkRed, width: 4),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: silverBg,
child: Column(
children: [
// Message Box
Container(
width: double.infinity,
color: messageBoxBg,
padding: const EdgeInsets.all(24),
child: const Text(
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.normal,
height: 1.5,
),
),
),
const SizedBox(height: 16),
// Stats Row
Row(
children: [
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"STREAK",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
streakText,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"SEEN",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
"$_seenCount/${AppConstants.totalPokemon}",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"SCORE",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
"$score",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
// Try Again Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, true);
},
icon: const Icon(Icons.refresh, color: Colors.white, size: 24),
label: const Text(
"TRY AGAIN",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: tryAgainBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(height: 16),
// Back to Pokedex Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, false);
},
icon: const Icon(Icons.menu_book, color: Colors.white, size: 24),
label: const Text(
"BACK TO POKEDEX",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: backBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
), ),
), ),
), ],
], ),
), ),
), ),
),
); );
} }
} }
+31 -322
View File
@@ -1,12 +1,17 @@
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 '../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 +27,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;
@@ -41,7 +45,6 @@ class _GuessPageState extends ConsumerState<GuessPage> {
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 state = ref.read(gameProvider); final state = ref.read(gameProvider);
switch (result) { switch (result) {
case GuessResult.correct: case GuessResult.correct:
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
@@ -81,16 +84,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);
@@ -104,260 +101,42 @@ class _GuessPageState extends ConsumerState<GuessPage> {
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), selectedGens: ref.watch(genFilterProvider),
decoration: BoxDecoration( onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: isGuessed
? PokemonImage(
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
)
: PokemonImage(
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
color: state.isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
state.isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: state.isShiny ? Colors.yellow[400] : Colors.white,
fontSize: state.isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
)
],
),
),
// Gen filter
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => setState(() => _genFilterOpen = !_genFilterOpen),
icon: Icon(
_genFilterOpen ? Icons.expand_less : Icons.filter_list,
color: const Color(0xFF1B2333),
),
label: const Text(
'GEN FILTER',
style: TextStyle(
color: Color(0xFF1B2333),
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 2,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF1B2333), width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: _genFilterOpen
? _GenFilterPanel(
selectedGens: ref.watch(genFilterProvider),
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
)
: const SizedBox.shrink(),
),
],
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// 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 +147,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),
),
],
),
),
);
}),
),
);
}
}
+1
View File
@@ -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);
+54 -244
View File
@@ -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),
),
),
);
}
} }
+24 -65
View File
@@ -3,7 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart'; import '../../domain/entities/pokemon.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);
@@ -35,21 +39,7 @@ 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,
@@ -60,62 +50,20 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
], ],
), ),
), ),
// 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('Erreur de chargement\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,7 +75,6 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
], ],
), ),
), ),
// Footer
Container( Container(
height: 24, height: 24,
color: const Color(0xFF1B2333), color: const Color(0xFF1B2333),
@@ -140,6 +87,20 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
); );
} }
Widget _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)),
],
),
);
}
Widget _buildTab(String title, bool isSelected) { Widget _buildTab(String title, bool isSelected) {
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
@@ -159,9 +120,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
alignment: Alignment.center, alignment: Alignment.center,
child: Text(title, child: Text(title,
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)),
), ),
), ),
); );
+14 -194
View File
@@ -4,7 +4,12 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart'; import '../../core/config/app_constants.dart';
import '../providers/pokedex_provider.dart'; import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart'; import '../providers/theme_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';
/// Page "Système" : statistiques de jeu et sélection de la palette de couleurs.
class SystemPage extends ConsumerWidget { class SystemPage extends ConsumerWidget {
const SystemPage({super.key}); const SystemPage({super.key});
@@ -23,14 +28,14 @@ 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),
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: 'STATISTIQUES', color: palette.primary),
const SizedBox(height: 8), const SizedBox(height: 8),
pokedexAsync.when( pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
@@ -44,13 +49,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: 'Meilleur score', value: '$best', icon: Icons.emoji_events),
_StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon), StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon),
_StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility), StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility),
_StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart), StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart),
], ],
); );
}, },
@@ -58,9 +63,9 @@ class SystemPage extends ConsumerWidget {
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary), SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary),
const SizedBox(height: 8), const SizedBox(height: 8),
_PalettePicker( PalettePicker(
selectedIndex: paletteIndex, selectedIndex: paletteIndex,
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i), onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
), ),
@@ -73,188 +78,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);
@@ -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,131 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.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) {
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: [
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),
_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!}"'
: '"No description available for this Pokémon."',
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,44 @@
import 'package:flutter/material.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) {
return Column(
children: [
_button(label: "TRY AGAIN", icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain),
const SizedBox(height: 16),
_button(label: "BACK TO POKEDEX", 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,58 @@
import 'package:flutter/material.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) {
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: 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),
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),
],
),
),
);
}
}
@@ -0,0 +1,52 @@
import 'package:flutter/material.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) {
return Row(
children: [
Expanded(child: _StatBox(label: "STREAK", value: streak)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SEEN", value: seen)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SCORE", 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,132 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.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: 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: 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,136 @@
import 'package:flutter/material.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) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
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(
"HINT: ${_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: 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)
_bigButton(label: "CONTINUE", color: Colors.green, onPressed: onContinue)
else ...[
_bigButton(label: "GUESS!", color: const Color(0xFF3B6EE3), onPressed: onGuess),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _actionButton(icon: Icons.lightbulb, label: "HINT ($hints)", color: Colors.amber, onPressed: onHint),
),
const SizedBox(width: 8),
Expanded(
child: _actionButton(icon: Icons.skip_next, label: "SKIP ($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,70 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.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 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 ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
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,39 @@
import 'package:flutter/material.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) {
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: [
const Text(
"CURRENT SCORE",
style: 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(
"PERSONAL BEST: $bestScore",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
),
);
}
}
@@ -0,0 +1,29 @@
import 'package:flutter/material.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),
),
const Text('POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
],
),
);
}
}
@@ -0,0 +1,23 @@
import 'package:flutter/material.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: 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),
],
),
);
}
}
@@ -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);
+1 -3
View File
@@ -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,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,24 @@
import 'package:flutter/material.dart';
/// En-tête de la page Système.
class SystemHeader extends StatelessWidget {
final Color primaryColor;
const SystemHeader({super.key, required this.primaryColor});
@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),
),
);
}
}
@@ -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),
),
],
),
);
}
}