feat: Introduce PokemonImage component for robust image loading with fallback and shiny support, and update iOS platform to 15.0.

This commit is contained in:
2026-03-20 11:23:22 +01:00
parent 8cca5a64de
commit 4faf259aaa
10 changed files with 160 additions and 33 deletions
+87
View File
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
class PokemonImage extends StatelessWidget {
final String imageUrl;
final String? fallbackUrl;
final BoxFit fit;
final double? width;
final double? height;
final Color? color;
final BlendMode? colorBlendMode;
const PokemonImage({
super.key,
required this.imageUrl,
this.fallbackUrl,
this.fit = BoxFit.contain,
this.width,
this.height,
this.color,
this.colorBlendMode,
});
@override
Widget build(BuildContext context) {
return Image.network(
imageUrl,
fit: fit,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
errorBuilder: (context, error, stackTrace) {
// If the primary image fails and we have a fallback, try the fallback
if (fallbackUrl != null && fallbackUrl != imageUrl) {
return Image.network(
fallbackUrl!,
fit: fit,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
errorBuilder: (context, error, stackTrace) {
// If the fallback also fails, show a placeholder
return _buildPlaceholder();
},
);
}
// No fallback, show placeholder
return _buildPlaceholder();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
);
}
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.help_outline,
size: (width ?? 40) * 0.5,
color: Colors.grey[400],
),
if ((width ?? 100) > 60)
Text(
"Not Found",
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import 'pokemon_image.dart';
class PokemonTile extends StatelessWidget {
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
@@ -41,7 +42,7 @@ class PokemonTile extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: pokemon.isCaught
? Image.network(pokemon.imageUrl, fit: BoxFit.contain)
? PokemonImage(imageUrl: pokemon.imageUrl, fit: BoxFit.contain)
: const SizedBox.expand(),
),
const SizedBox(width: 16),
+5 -1
View File
@@ -1,5 +1,6 @@
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);
@@ -95,7 +96,10 @@ class _GameOverPageState extends State<GameOverPage> {
if (pokemonImage.isNotEmpty)
SizedBox(
height: 140,
child: Image.network(pokemonImage, fit: BoxFit.contain),
child: PokemonImage(
imageUrl: pokemonImage,
fit: BoxFit.contain,
),
),
const SizedBox(height: 12),
Text(
+12 -7
View File
@@ -4,6 +4,7 @@ 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);
@@ -258,13 +259,17 @@ class _GuessPageState extends State<GuessPage> {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _isGuessed
? Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain)
: ColorFiltered(
colorFilter: ColorFilter.mode(
_isShiny ? Colors.yellow[700]! : Colors.black,
BlendMode.srcIn
),
child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain),
? 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,
),
),
),
+6 -1
View File
@@ -1,6 +1,7 @@
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);
@@ -126,7 +127,11 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
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),
child: PokemonImage(
imageUrl: _isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: _isShiny ? pokemon.imageUrl : null,
fit: BoxFit.contain,
),
),
),
Container(