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:
Maxiwere45 2026-03-20 11:23:22 +01:00
parent 8cca5a64de
commit 4faf259aaa
10 changed files with 160 additions and 33 deletions

View File

@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '13.0' platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -39,5 +39,8 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end end
end end

View File

@ -6,25 +6,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.51.1): - sqlite3 (3.52.0):
- sqlite3/common (= 3.51.1) - sqlite3/common (= 3.52.0)
- sqlite3/common (3.51.1) - sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.51.1): - sqlite3/dbstatvtab (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.51.1): - sqlite3/fts5 (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.51.1): - sqlite3/math (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.51.1): - sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.51.1): - sqlite3/rtree (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.51.1): - sqlite3/session (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.51.1) - sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
@ -56,9 +56,9 @@ SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 4b015915ec662986b54bf30ab778da63f7dda016
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@ -1,13 +1,16 @@
import UIKit
import Flutter import Flutter
import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -24,6 +26,29 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@ -41,9 +66,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

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,
),
),
],
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/pokemon.dart'; import '../models/pokemon.dart';
import 'pokemon_image.dart';
class PokemonTile extends StatelessWidget { class PokemonTile extends StatelessWidget {
const PokemonTile(this.pokemon, {Key? key}) : super(key: key); const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
@ -41,7 +42,7 @@ class PokemonTile extends StatelessWidget {
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: pokemon.isCaught child: pokemon.isCaught
? Image.network(pokemon.imageUrl, fit: BoxFit.contain) ? PokemonImage(imageUrl: pokemon.imageUrl, fit: BoxFit.contain)
: const SizedBox.expand(), : const SizedBox.expand(),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../database/pokedex_database.dart'; import '../database/pokedex_database.dart';
import '../components/pokemon_image.dart';
class GameOverPage extends StatefulWidget { class GameOverPage extends StatefulWidget {
const GameOverPage({Key? key}) : super(key: key); const GameOverPage({Key? key}) : super(key: key);
@ -95,7 +96,10 @@ class _GameOverPageState extends State<GameOverPage> {
if (pokemonImage.isNotEmpty) if (pokemonImage.isNotEmpty)
SizedBox( SizedBox(
height: 140, height: 140,
child: Image.network(pokemonImage, fit: BoxFit.contain), child: PokemonImage(
imageUrl: pokemonImage,
fit: BoxFit.contain,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(

View File

@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/pokemon.dart'; import '../models/pokemon.dart';
import '../database/pokedex_database.dart'; import '../database/pokedex_database.dart';
import 'main_page.dart'; import 'main_page.dart';
import '../components/pokemon_image.dart';
class GuessPage extends StatefulWidget { class GuessPage extends StatefulWidget {
const GuessPage({Key? key}) : super(key: key); const GuessPage({Key? key}) : super(key: key);
@ -258,13 +259,17 @@ class _GuessPageState extends State<GuessPage> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: _isGuessed child: _isGuessed
? Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain) ? PokemonImage(
: ColorFiltered( imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
colorFilter: ColorFilter.mode( fallbackUrl: _currentPokemon!.imageUrl,
_isShiny ? Colors.yellow[700]! : Colors.black, fit: BoxFit.contain,
BlendMode.srcIn )
), : PokemonImage(
child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain), imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
fallbackUrl: _currentPokemon!.imageUrl,
fit: BoxFit.contain,
color: _isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
), ),
), ),
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/pokemon.dart'; import '../models/pokemon.dart';
import '../components/pokemon_type.dart'; import '../components/pokemon_type.dart';
import '../components/pokemon_image.dart';
class PokemonDetailPage extends StatefulWidget { class PokemonDetailPage extends StatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key); const PokemonDetailPage({Key? key}) : super(key: key);
@ -126,7 +127,11 @@ class _PokemonDetailPageState extends State<PokemonDetailPage> {
height: 180, height: 180,
alignment: Alignment.center, alignment: Alignment.center,
color: const Color(0xFF81CCA5).withAlpha(153), // subtle green background behind sprite 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( Container(