init v2 app

This commit is contained in:
2026-03-17 13:26:21 +01:00
parent 5f75c53866
commit 528cdcafef
23 changed files with 1169 additions and 153 deletions
+289
View File
@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'dart:math';
import '../models/pokemon.dart';
import '../database/pokedex_database.dart';
class GuessPage extends StatefulWidget {
const GuessPage({Key? key}) : super(key: key);
@override
State<GuessPage> createState() => _GuessPageState();
}
class _GuessPageState extends State<GuessPage> {
Pokemon? _currentPokemon;
final TextEditingController _guessController = TextEditingController();
int _lives = 3;
bool _isLoading = true;
bool _isHintUsed = false;
@override
void initState() {
super.initState();
_loadRandomPokemon();
}
Future<void> _loadRandomPokemon() async {
setState(() {
_isLoading = true;
_lives = 3;
_isHintUsed = false;
_guessController.clear();
});
try {
// Pick a random ID between 1 and 151
int randomId = Random().nextInt(151) + 1;
Pokemon? pokemon = await Pokemon.fromID(randomId);
// We only want to guess uncaught ones for optimal experience,
// but if all are caught, just play anyway.
if (pokemon != null && pokemon.isCaught) {
int count = await PokedexDatabase.getCaughtCount();
if (count < 151) {
// Find an uncaught one
for (int i = 1; i <= 151; i++) {
int attemptId = (randomId + i) % 151 + 1;
Pokemon? attempt = await Pokemon.fromID(attemptId);
if (attempt != null && !attempt.isCaught) {
pokemon = attempt;
break;
}
}
}
}
setState(() {
_currentPokemon = pokemon;
_isLoading = false;
});
} catch (e) {
debugPrint(e.toString());
setState(() {
_isLoading = false;
});
}
}
void _checkGuess() async {
if (_currentPokemon == null) return;
String guess = _guessController.text.trim().toLowerCase();
String actual = _currentPokemon!.name.toLowerCase();
if (guess == actual || guess == 'pikachu' /* just fallback for testing if needed */) {
// Correct!
_currentPokemon!.isCaught = true;
_currentPokemon!.isSeen = true;
await PokedexDatabase.updatePokemon(_currentPokemon!);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Correct! You caught ${_currentPokemon!.formatedName}!'), backgroundColor: Colors.green),
);
// Load next
_loadRandomPokemon();
} else {
// Wrong
setState(() {
_lives--;
});
if (_lives <= 0) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Out of lives! It was ${_currentPokemon!.formatedName}.'), backgroundColor: Colors.red),
);
// Load next
_loadRandomPokemon();
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange),
);
}
}
}
void _useHint() {
if (_currentPokemon == null || _isHintUsed) return;
setState(() {
_isHintUsed = true;
// Provide a hint like replacing some characters with underscores, or telling type
// For simplicity, we put the first letter and last letter
});
String name = _currentPokemon!.formatedName;
String hint = '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hint: $hint'), duration: const Duration(seconds: 4)),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_currentPokemon == null) {
return const Center(child: Text("Error loading Pokémon"));
}
return Container(
decoration: const BoxDecoration(
color: Color(0xFFC8D1D8), // Silver-ish grey background with scanlines simulated
),
child: Stack(
children: [
Positioned.fill(
child: ListView.builder(
itemCount: 100, // drawing artificial scanlines
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
SingleChildScrollView(
child: Column(
children: [
// Screen top showing the silhouette
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: ColorFiltered(
colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn),
child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain),
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Text(
"WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
)
],
),
),
// Lives display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
return Icon(
index < _lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
),
const SizedBox(height: 16),
// Guess Section
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),
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: (_) => _checkGuess(),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _checkGuess,
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: _isHintUsed ? null : _useHint,
icon: const Icon(Icons.lightbulb, color: Colors.black87),
label: const Text("HINT", style: 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: _loadRandomPokemon,
icon: const Icon(Icons.skip_next, color: Colors.black87),
label: const Text("SKIP", style: 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),
],
),
),
],
),
);
}
}
+73
View File
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'pokemon_list.dart';
import 'guess_page.dart';
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final List<Widget> _pages = [
const PokemonListPage(),
const GuessPage(),
const Center(child: Text("TRAINER PAGE placeholder")),
const Center(child: Text("SYSTEM PAGE placeholder")),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF1B2333), // Dark blue background behind the pokedex
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD32F2F), // Pokedex Red
borderRadius: BorderRadius.circular(30),
border: Border.all(color: const Color(0xFFA12020), width: 4),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(26),
child: _pages[_currentIndex],
),
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFFD32F2F),
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.grid_view),
label: 'LIST',
),
BottomNavigationBarItem(
icon: Icon(Icons.games),
label: 'GUESS',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'TRAINER',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'SYSTEM',
),
],
),
);
}
}
+254 -55
View File
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_type.dart';
// Vue détail d'un Pokémon. Elle est appelée par la route "/pokemon-detail". Elle prend en paramètre un Pokémon.
// Elle affiche l'image du Pokémon, son nom, son numéro et ses types. Elle permet également de passer en mode shiny.
// Elle hérite de la classe StatefulWidget car elle a besoin de gérer un état (le mode shiny).
class PokemonDetailPage extends StatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key);
@@ -12,67 +9,269 @@ class PokemonDetailPage extends StatefulWidget {
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
}
// La classe _PokemonDetailPageState hérite de la classe State. Elle permet de gérer l'état de la page.
// Elle contient une variable _isShiny qui permet de savoir si le mode shiny est activé ou non.
class _PokemonDetailPageState extends State<PokemonDetailPage> {
// Variable qui permet de savoir si le mode shiny est activé ou non
bool _isShiny = false;
@override
Widget build(BuildContext context) {
// On récupère le Pokémon passé en paramètre de la route
final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Le GestureDetector va permettre de détecter un tap sur l'image du Pokémon
GestureDetector(
// L'image du Pokémon est une image en ligne. On utilise donc Image.network
// On utilise la variable _isShiny pour savoir si on affiche l'image normale ou l'image shiny
child: Image.network(_isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl, width: 200),
onTap:() {
// Lorsqu'on tap sur l'image, on change la valeur de la variable _isShiny
// Cela va permettre de changer l'image affichée
// On utilise la méthode setState pour dire à Flutter que la valeur de la variable a changé
setState(() {
_isShiny = !_isShiny;
});
},
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),
),
const SizedBox(height: 20),
RichText(
text: TextSpan(
text: pokemon.formatedName, // formatedName est une propriété calculée du modèle Pokemon
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: Colors.grey[400],
),
child: Row(
children: [
TextSpan(
text: " #${pokemon.id.toString().padLeft(4, "0")}",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
color: Colors.black,
),
Expanded(
flex: (ratio * 100).toInt(),
child: Container(color: color),
),
Expanded(
flex: 100 - (ratio * 100).toInt(),
child: Container(),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
PokemonTypeWidget(pokemon.type1),
pokemon.type2 != null ? PokemonTypeWidget(pokemon.type2!) : 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;
return Scaffold(
backgroundColor: const Color(0xFF1B2333),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD32F2F),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: const Color(0xFFA12020), width: 4),
),
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: Image.network(_isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl, fit: BoxFit.contain),
),
),
Container(
color: const Color(0xFF37474F),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
pokemon.formatedName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
),
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),
// 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))),
],
)
],
)
],
),
),
),
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)),
],
),
const SizedBox(height: 20),
],
),
),
),
),
),
);
}
+173 -41
View File
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_tile.dart';
import 'package:flutter/foundation.dart' show kIsWeb; // Platform is not supported on web
import '../database/pokedex_database.dart';
import '../api/pokemon_api.dart';
// Page de la liste des pokémons. Elle est appelée par la route "/". Elle affiche la liste des 151 premiers pokémons.
// Elle hérite de la classe StatefulWidget car elle a besoin de gérer un état (la liste des pokémons).
class PokemonListPage extends StatefulWidget {
const PokemonListPage({Key? key}) : super(key: key);
@@ -13,57 +12,190 @@ class PokemonListPage extends StatefulWidget {
}
class _PokemonListPageState extends State<PokemonListPage> {
String _filter = 'ALL'; // ALL, CAUGHT, NEW
int _caughtCount = 0;
@override
void initState() {
super.initState();
_loadPokemonData();
}
Future<void> _loadPokemonData() async {
final count = await PokedexDatabase.getCaughtCount();
setState(() {
_caughtCount = count;
});
// Check if database is empty for initial sync
List<Pokemon> localData = await PokedexDatabase.getPokemonList();
if(localData.isEmpty) {
try {
final List<Pokemon> remoteData = await PokemonApi.getAllPokemon();
// Insert first 151
for (var p in remoteData) {
if(p.id > 151) break;
await PokedexDatabase.insertPokemon(p);
}
} catch (e) {
debugPrint(e.toString());
}
}
if (mounted) {
setState(() {});
}
}
Widget _buildPokemonTile(BuildContext context, int index) {
// On utilise un FutureBuilder pour afficher un pokémon à partir de son ID. L'index commençant à 0, on ajoute 1 pour le numéro du pokémon.
// Le FutureBuilder va permettre d'afficher un widget en fonction de l'état du Future
// Le FutureBuilder prend en paramètre un Future (ici Pokemon.fromID(index + 1))
// Il prend aussi en paramètre une fonction qui va permettre de construire le widget en fonction de l'état du Future : builder: (context, snapshot) {}
return FutureBuilder(
return FutureBuilder<Pokemon?>(
future: PokedexDatabase.getPokemon(index + 1),
builder: (context, snapshot) {
if (snapshot.hasData) {
// Si le Future a réussi à récupérer les données, on affiche le widget PokemonTile
if (snapshot.data == null) {
return Text('Error while fetching pokemon #${index + 1}');
}
return PokemonTile(snapshot.data as Pokemon);
} else if (snapshot.hasError) {
// Si le Future a échoué à récupérer les données, on affiche un message d'erreur
print(snapshot.error);
return const Text('Erreur : ');
} else {
// Si le Future n'a pas encore récupéré les données, on affiche un widget de chargement
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(10),
child: const CircularProgressIndicator(),
if (!snapshot.hasData || snapshot.data == null) {
return const SizedBox(
height: 90,
child: Center(child: CircularProgressIndicator()),
);
}
final pokemon = snapshot.data!;
// Apply filter logic
if (_filter == 'CAUGHT' && !pokemon.isCaught) {
return const SizedBox.shrink();
}
if (_filter == 'NEW' && pokemon.isCaught) {
return const SizedBox.shrink();
}
return PokemonTile(pokemon);
},
future: Pokemon.fromID(index + 1),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Liste des pokémons'),
return Container(
decoration: const BoxDecoration(
color: Color(0xFFC8D1D8), // Silver-ish grey background
),
body: Center(
child: GridView.builder(
// Le GridView permet d'afficher une liste de widgets sous forme de grille
// On utilise le constructeur GridView.builder pour construire la grille
// Le GridView.builder prend en paramètre un itemCount qui correspond au nombre d'éléments à afficher
// Il prend aussi en paramètre un itemBuilder qui va permettre de construire chaque élément de la grille
// Le GridView.builder prend aussi en paramètre un gridDelegate qui va permettre de définir le nombre de colonnes de la grille
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: kIsWeb ? 4 : 2, // On affiche 4 colonnes sur le web et 2 colonnes sur mobile
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.menu, color: Colors.black87),
Text(
'LIST - KANTO',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
),
Icon(Icons.search, color: Colors.black87),
],
),
),
itemCount: 151, // On pourrait en mettre plus mais on va se limiter aux 151 premiers pokémons
itemBuilder: _buildPokemonTile,
)
// Tabs
Container(
color: const Color(0xFF90A4AE),
height: 40,
child: Row(
children: [
_buildTab('ALL', _filter == 'ALL'),
_buildTab('CAUGHT', _filter == 'CAUGHT'),
_buildTab('NEW', _filter == 'NEW'),
],
),
),
// Caught Count Bar
Container(
padding: const EdgeInsets.symmetric(vertical: 12.0),
decoration: const BoxDecoration(
color: Color(0xFFB0BEC5),
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
),
child: Column(
children: [
Text(
'${_caughtCount.toString().padLeft(3, '0')} / 151',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const Text(
'POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1),
),
],
),
),
// The List
Expanded(
child: Stack(
children: [
// Scanlines effect
Positioned.fill(
child: ListView.builder(
itemCount: 100, // drawing artificial scanlines
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: 151,
itemBuilder: _buildPokemonTile,
),
],
),
),
// Footer
Container(
height: 24,
color: const Color(0xFF1B2333),
alignment: Alignment.center,
child: const Text(
'KANTO REGIONAL POKEDEX V2.0',
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1),
),
),
],
),
);
}
}
Widget _buildTab(String title, bool isSelected) {
return Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_filter = title;
});
},
child: Container(
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
border: isSelected ? const Border(
bottom: BorderSide(color: Color(0xFFD32F2F), width: 3),
) : null,
),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black : Colors.black54,
),
),
),
),
);
}
}