From 41a80f03f036a4aa915ec5cf057976c0f8a2a4b2 Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Tue, 23 Jun 2026 11:49:19 +0200 Subject: [PATCH] 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 --- lib/presentation/pages/pokemon_detail.dart | 300 ++++-------------- .../widgets/detail/pokemon_detail_top.dart | 83 +++++ .../widgets/detail/pokemon_stats_panel.dart | 131 ++++++++ 3 files changed, 269 insertions(+), 245 deletions(-) create mode 100644 lib/presentation/widgets/detail/pokemon_detail_top.dart create mode 100644 lib/presentation/widgets/detail/pokemon_stats_panel.dart diff --git a/lib/presentation/pages/pokemon_detail.dart b/lib/presentation/pages/pokemon_detail.dart index 5fc701c..4971709 100644 --- a/lib/presentation/pages/pokemon_detail.dart +++ b/lib/presentation/pages/pokemon_detail.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import '../../domain/entities/pokemon.dart'; -import '../widgets/pokemon_type.dart'; -import '../widgets/pokemon_image.dart'; +import '../widgets/detail/pokemon_detail_top.dart'; +import '../widgets/detail/pokemon_stats_panel.dart'; +/// Fiche détaillée d'un Pokémon (reçu via les arguments de route). class PokemonDetailPage extends StatefulWidget { const PokemonDetailPage({Key? key}) : super(key: key); @@ -13,57 +14,9 @@ class PokemonDetailPage extends StatefulWidget { class _PokemonDetailPageState extends State { 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 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( backgroundColor: const Color(0xFF1B2333), @@ -79,202 +32,18 @@ class _PokemonDetailPageState extends State { child: SingleChildScrollView( child: Column( children: [ - // App Bar / Top Red Padding - 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), - ), - ), - ), - - // 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)), - ], + _backBar(context), + PokemonDetailTop( + pokemon: pokemon, + isShiny: _isShiny, + onToggleShiny: () => setState(() => _isShiny = !_isShiny), ), const SizedBox(height: 20), - - // BOTTOM SCREEN - 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), - - _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))), - ], - ) - ], - ) - ], - ), - ), - ), + _hinge(), + const SizedBox(height: 20), + PokemonStatsPanel(pokemon: pokemon), const SizedBox(height: 30), - - // 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)), - ], - ), + _bottomDots(), const SizedBox(height: 20), ], ), @@ -284,4 +53,45 @@ class _PokemonDetailPageState extends State { ), ); } -} \ No newline at end of file + + 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), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/detail/pokemon_detail_top.dart b/lib/presentation/widgets/detail/pokemon_detail_top.dart new file mode 100644 index 0000000..b5e87e0 --- /dev/null +++ b/lib/presentation/widgets/detail/pokemon_detail_top.dart @@ -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!), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/detail/pokemon_stats_panel.dart b/lib/presentation/widgets/detail/pokemon_stats_panel.dart new file mode 100644 index 0000000..7b5bbbc --- /dev/null +++ b/lib/presentation/widgets/detail/pokemon_stats_panel.dart @@ -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))), + ], + ), + ], + ); + } +}