refactor: move UI into presentation/widgets and presentation/pages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,306 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../database/pokedex_database.dart';
|
||||
import '../components/pokemon_image.dart';
|
||||
|
||||
class GameOverPage extends StatefulWidget {
|
||||
const GameOverPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GameOverPage> createState() => _GameOverPageState();
|
||||
}
|
||||
|
||||
class _GameOverPageState extends State<GameOverPage> {
|
||||
int _seenCount = 0;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSeenCount();
|
||||
}
|
||||
|
||||
Future<void> _loadSeenCount() async {
|
||||
int count = await PokedexDatabase.getSeenCount();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_seenCount = count;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, dynamic>? args =
|
||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
|
||||
|
||||
final String pokemonImage = args?['pokemonImage'] ?? '';
|
||||
final String pokemonName = args?['pokemonName'] ?? 'Unknown';
|
||||
final int streak = args?['streak'] ?? 0;
|
||||
|
||||
// Pad streak with zeroes to 3 digits as in mockup (e.g. 004)
|
||||
final String streakText = streak.toString().padLeft(3, '0');
|
||||
|
||||
// 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(
|
||||
backgroundColor: pokedexRed,
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator(color: Colors.white))
|
||||
: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Box: Pokemon Silhouette & GAME OVER
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: darkRed, // Border color
|
||||
border: Border.all(color: darkRed, width: 4),
|
||||
),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
||||
color: silverBg,
|
||||
child: Column(
|
||||
children: [
|
||||
// GAME OVER Banner
|
||||
Container(
|
||||
color: darkRed,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: const Text(
|
||||
"GAME OVER",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.yellow,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1.5, 1.5),
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Pokemon Image and Name
|
||||
if (pokemonImage.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 140,
|
||||
child: PokemonImage(
|
||||
imageUrl: pokemonImage,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"It was $pokemonName!",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1B2333),
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Divider between boxes
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(height: 2, color: darkRed),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(3, (index) =>
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: darkRed,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(height: 2, color: darkRed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Box: Message, Stats, Buttons
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: darkRed,
|
||||
border: Border.all(color: darkRed, width: 4),
|
||||
),
|
||||
child: Container(
|
||||
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/1025", // Gen 9 total
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/pokemon.dart';
|
||||
import '../database/pokedex_database.dart';
|
||||
import 'main_page.dart';
|
||||
import '../components/pokemon_image.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;
|
||||
int _skips = 3;
|
||||
int _hints = 3;
|
||||
int _sessionCorrectCount = 0;
|
||||
bool _isGuessed = false;
|
||||
bool _isLoading = true;
|
||||
bool _isHintUsed = false;
|
||||
bool _isShiny = false;
|
||||
int _currentScore = 0;
|
||||
int _bestScore = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadBestScore();
|
||||
_startNewGame();
|
||||
}
|
||||
|
||||
void _startNewGame() {
|
||||
setState(() {
|
||||
_lives = 3;
|
||||
_skips = 3;
|
||||
_hints = 3;
|
||||
_sessionCorrectCount = 0;
|
||||
_currentScore = 0;
|
||||
});
|
||||
_loadRandomPokemon();
|
||||
}
|
||||
|
||||
Future<void> _loadBestScore() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_bestScore = prefs.getInt('best_score') ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveBestScore() async {
|
||||
if (_currentScore > _bestScore) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('best_score', _currentScore);
|
||||
setState(() {
|
||||
_bestScore = _currentScore;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadRandomPokemon() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_isGuessed = false;
|
||||
_isHintUsed = false;
|
||||
_isShiny = Random().nextInt(10) == 0; // 10% chance for shiny
|
||||
_guessController.clear();
|
||||
});
|
||||
|
||||
try {
|
||||
// Pick a random ID between 1 and 1025 (Gen 9)
|
||||
int randomId = Random().nextInt(1025) + 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 < 1025) {
|
||||
// Find an uncaught one
|
||||
for (int i = 1; i <= 1025; i++) {
|
||||
int attemptId = (randomId + i) % 1025 + 1;
|
||||
Pokemon? attempt = await Pokemon.fromID(attemptId);
|
||||
if (attempt != null && !attempt.isCaught) {
|
||||
pokemon = attempt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentPokemon = pokemon;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _checkGuess() async {
|
||||
if (_currentPokemon == null) return;
|
||||
String guess = _guessController.text.trim().toLowerCase();
|
||||
String actual = _currentPokemon!.name.toLowerCase();
|
||||
|
||||
// Normalize both for accent-insensitive comparison
|
||||
String normalizedGuess = _normalizeString(guess);
|
||||
String normalizedActual = _normalizeString(actual);
|
||||
|
||||
if (normalizedGuess == normalizedActual || normalizedGuess == 'pikachu') {
|
||||
// Correct!
|
||||
_currentPokemon!.isCaught = true;
|
||||
_currentPokemon!.isSeen = true;
|
||||
await PokedexDatabase.updatePokemon(_currentPokemon!);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentScore += _isShiny ? 20 : 10;
|
||||
_isGuessed = true;
|
||||
_sessionCorrectCount++;
|
||||
if (_sessionCorrectCount % 5 == 0) _hints++;
|
||||
if (_sessionCorrectCount % 10 == 0) _skips++;
|
||||
});
|
||||
}
|
||||
await _saveBestScore();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_isShiny
|
||||
? '✨ SHINY! You caught ${_currentPokemon!.formatedName}! (+20 pts) ✨'
|
||||
: 'Correct! You caught ${_currentPokemon!.formatedName}!'),
|
||||
backgroundColor: _isShiny ? Colors.amber[800] : Colors.green
|
||||
),
|
||||
);
|
||||
// Wait for user to click Continue
|
||||
} else {
|
||||
// Wrong
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lives--;
|
||||
});
|
||||
}
|
||||
|
||||
if (_lives <= 0) {
|
||||
if (!mounted) return;
|
||||
final bool? playAgain = await Navigator.pushNamed(
|
||||
context,
|
||||
'/game-over',
|
||||
arguments: {
|
||||
'pokemonName': _currentPokemon!.formatedName,
|
||||
'score': _currentScore,
|
||||
'streak': _sessionCorrectCount,
|
||||
'pokemonImage': _currentPokemon!.imageUrl,
|
||||
},
|
||||
) as bool?;
|
||||
|
||||
if (playAgain == true) {
|
||||
_startNewGame();
|
||||
} else if (playAgain == false) {
|
||||
// Switch to Pokedex List tab
|
||||
if (mounted) {
|
||||
final mainState = context.findAncestorStateOfType<MainPageState>();
|
||||
mainState?.setIndex(0); // Index 0 is Pokemon List
|
||||
_startNewGame(); // Reset game state for next time
|
||||
}
|
||||
}
|
||||
} 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 || _hints <= 0) return;
|
||||
setState(() {
|
||||
_isHintUsed = true;
|
||||
_hints--;
|
||||
});
|
||||
}
|
||||
|
||||
void _useSkip() {
|
||||
if (_skips > 0) {
|
||||
setState(() {
|
||||
_skips--;
|
||||
});
|
||||
_loadRandomPokemon();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeString(String input) {
|
||||
var withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
|
||||
var withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
|
||||
|
||||
String output = input;
|
||||
for (int i = 0; i < withDia.length; i++) {
|
||||
output = output.replaceAll(withDia[i], withoutDia[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@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: _isGuessed
|
||||
? PokemonImage(
|
||||
imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
|
||||
fallbackUrl: _currentPokemon!.imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: PokemonImage(
|
||||
imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
|
||||
fallbackUrl: _currentPokemon!.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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
if (_isHintUsed && _currentPokemon != null)
|
||||
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: ${_currentPokemon!.formatedName[0]}${List.filled(_currentPokemon!.formatedName.length - 2, '_').join()}${_currentPokemon!.formatedName[_currentPokemon!.formatedName.length - 1]}",
|
||||
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: (_) => _checkGuess(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_isGuessed)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
child: ElevatedButton(
|
||||
onPressed: _loadRandomPokemon,
|
||||
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: _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 || _hints <= 0) ? null : _useHint,
|
||||
icon: const Icon(Icons.lightbulb, color: Colors.black87),
|
||||
label: Text("HINT ($_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: _skips > 0 ? _useSkip : null,
|
||||
icon: const Icon(Icons.skip_next, color: Colors.black87),
|
||||
label: Text("SKIP ($_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 Display
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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;
|
||||
|
||||
void setIndex(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
final List<Widget> _pages = [
|
||||
const PokemonListPage(),
|
||||
const GuessPage(),
|
||||
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: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _pages,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: 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.settings),
|
||||
label: 'SYSTEM',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/pokemon.dart';
|
||||
import '../components/pokemon_type.dart';
|
||||
import '../components/pokemon_image.dart';
|
||||
|
||||
class PokemonDetailPage extends StatefulWidget {
|
||||
const PokemonDetailPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
|
||||
}
|
||||
|
||||
class _PokemonDetailPageState extends State<PokemonDetailPage> {
|
||||
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;
|
||||
|
||||
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: 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),
|
||||
|
||||
// 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/pokemon.dart';
|
||||
import '../components/pokemon_tile.dart';
|
||||
import '../database/pokedex_database.dart';
|
||||
import '../api/pokemon_api.dart';
|
||||
|
||||
class PokemonListPage extends StatefulWidget {
|
||||
const PokemonListPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PokemonListPage> createState() => _PokemonListPageState();
|
||||
}
|
||||
|
||||
class _PokemonListPageState extends State<PokemonListPage> {
|
||||
String _filter = 'ALL'; // ALL, CAUGHT, NEW
|
||||
int _caughtCount = 0;
|
||||
List<Pokemon> _allPokemon = [];
|
||||
List<Pokemon> _filteredPokemon = [];
|
||||
bool _isSyncing = false;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPokemonData();
|
||||
PokedexDatabase.onDatabaseUpdate.addListener(_loadPokemonData);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
PokedexDatabase.onDatabaseUpdate.removeListener(_loadPokemonData);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadPokemonData() async {
|
||||
setState(() => _isSyncing = true);
|
||||
|
||||
final count = await PokedexDatabase.getCaughtCount();
|
||||
|
||||
// Check if database needs sync (less than 1025 pokemon)
|
||||
List<Pokemon> localData = await PokedexDatabase.getPokemonList();
|
||||
if (localData.length < 1025) {
|
||||
try {
|
||||
final List<Pokemon> remoteData = await PokemonApi.getAllPokemon();
|
||||
// Insert all missing pokemon using batch for performance
|
||||
await PokedexDatabase.batchInsertPokemon(remoteData);
|
||||
localData = await PokedexDatabase.getPokemonList();
|
||||
} catch (e) {
|
||||
debugPrint('Sync Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ID to ensure order
|
||||
localData.sort((a, b) => a.id.compareTo(b.id));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_allPokemon = localData;
|
||||
_caughtCount = count;
|
||||
_applyFilter();
|
||||
_isSyncing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _applyFilter() {
|
||||
setState(() {
|
||||
if (_filter == 'ALL') {
|
||||
_filteredPokemon = _allPokemon;
|
||||
} else if (_filter == 'CAUGHT') {
|
||||
_filteredPokemon = _allPokemon.where((p) => p.isCaught).toList();
|
||||
}
|
||||
});
|
||||
|
||||
// Reset scroll position to top when filter changes
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPokemonTile(BuildContext context, int index) {
|
||||
final pokemon = _filteredPokemon[index];
|
||||
return PokemonTile(pokemon);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFC8D1D8), // Silver-ish grey background
|
||||
),
|
||||
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 - NATIONAL',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
|
||||
),
|
||||
Icon(Icons.search, color: Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Tabs
|
||||
Container(
|
||||
color: const Color(0xFF90A4AE),
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTab('ALL', _filter == 'ALL'),
|
||||
_buildTab('CAUGHT', _filter == 'CAUGHT'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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')} / ${_allPokemon.length}',
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSyncing && _allPokemon.isEmpty)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_filteredPokemon.isEmpty)
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: _filteredPokemon.length,
|
||||
itemBuilder: _buildPokemonTile,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Footer
|
||||
Container(
|
||||
height: 24,
|
||||
color: const Color(0xFF1B2333),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'NATIONAL POKEDEX V2.0',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(String title, bool isSelected) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_filter != title) {
|
||||
_filter = title;
|
||||
_applyFilter();
|
||||
}
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user