diff --git a/lib/presentation/pages/guess_page.dart b/lib/presentation/pages/guess_page.dart index 943a229..a5d1c97 100644 --- a/lib/presentation/pages/guess_page.dart +++ b/lib/presentation/pages/guess_page.dart @@ -1,12 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/config/app_constants.dart'; import '../../domain/game/game_state.dart'; import '../providers/gen_filter_provider.dart'; import '../providers/game_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 { const GuessPage({Key? key}) : super(key: key); @@ -22,7 +27,6 @@ class _GuessPageState extends ConsumerState { @override void initState() { super.initState(); - // Démarre la partie après le premier frame (le provider est prêt). WidgetsBinding.instance.addPostFrameCallback((_) { if (!_started) { _started = true; @@ -41,7 +45,6 @@ class _GuessPageState extends ConsumerState { final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text); if (!mounted) return; final state = ref.read(gameProvider); - switch (result) { case GuessResult.correct: ScaffoldMessenger.of(context).showSnackBar(SnackBar( @@ -81,16 +84,10 @@ class _GuessPageState extends ConsumerState { if (playAgain == false) { ref.read(selectedTabProvider.notifier).set(0); // onglet LIST } - // true (Try Again), false (Back to Pokédex) et null (geste retour système) - // relancent tous une nouvelle partie pour ne pas rester bloqué en game over. + // true / false / null (geste retour) relancent tous une partie pour ne pas rester bloqué. 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 Widget build(BuildContext context) { final state = ref.watch(gameProvider); @@ -104,260 +101,42 @@ class _GuessPageState extends ConsumerState { final pokemon = state.currentPokemon!; final isGuessed = state.status == GameStatus.roundWon; + final notifier = ref.read(gameProvider.notifier); return Container( decoration: const BoxDecoration(color: Color(0xFFC8D1D8)), child: Stack( children: [ - 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), - ), - ), - ), + const ScanlineOverlay(), SingleChildScrollView( child: Column( children: [ - // Silhouette screen - 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: 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(), - ), - ], - ), + GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed), + GenFilterSection( + isOpen: _genFilterOpen, + onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen), + selectedGens: ref.watch(genFilterProvider), + onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i), ), const SizedBox(height: 12), - // 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, - ); - }), - ), + LivesRow(lives: state.lives), 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: 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 (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)), - ], - ), - ), - ], - ), + child: ScoreBoard(currentScore: state.currentScore, bestScore: state.bestScore), ), const SizedBox(height: 32), ], @@ -368,73 +147,3 @@ class _GuessPageState extends ConsumerState { ); } } - -class _GenFilterPanel extends StatelessWidget { - final Set 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), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/lib/presentation/widgets/guess/gen_filter_section.dart b/lib/presentation/widgets/guess/gen_filter_section.dart new file mode 100644 index 0000000..4863515 --- /dev/null +++ b/lib/presentation/widgets/guess/gen_filter_section.dart @@ -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 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 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), + ), + ], + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/presentation/widgets/guess/guess_input_section.dart b/lib/presentation/widgets/guess/guess_input_section.dart new file mode 100644 index 0000000..6fd5494 --- /dev/null +++ b/lib/presentation/widgets/guess/guess_input_section.dart @@ -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), + ), + ); + } +} diff --git a/lib/presentation/widgets/guess/guess_silhouette.dart b/lib/presentation/widgets/guess/guess_silhouette.dart new file mode 100644 index 0000000..adb941e --- /dev/null +++ b/lib/presentation/widgets/guess/guess_silhouette.dart @@ -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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/guess/lives_row.dart b/lib/presentation/widgets/guess/lives_row.dart new file mode 100644 index 0000000..7760b63 --- /dev/null +++ b/lib/presentation/widgets/guess/lives_row.dart @@ -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, + ); + }), + ); + } +} diff --git a/lib/presentation/widgets/guess/score_board.dart b/lib/presentation/widgets/guess/score_board.dart new file mode 100644 index 0000000..dcc9b92 --- /dev/null +++ b/lib/presentation/widgets/guess/score_board.dart @@ -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), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/scanline_overlay.dart b/lib/presentation/widgets/scanline_overlay.dart new file mode 100644 index 0000000..dc89869 --- /dev/null +++ b/lib/presentation/widgets/scanline_overlay.dart @@ -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), + ), + ), + ); + } +}