Compare commits
42 Commits
main
...
pokeguess-
| Author | SHA1 | Date | |
|---|---|---|---|
| d54e517ca6 | |||
| 43b68d1352 | |||
| 5c9cef99a7 | |||
| ca77317a20 | |||
| dc7e681508 | |||
| e62c8a9034 | |||
| 2c21b80a03 | |||
| 4ac13bd233 | |||
| 7fd6d16020 | |||
| d4c8936c80 | |||
| 439c0101f4 | |||
| 29d2c92b37 | |||
| 4bca4b1ed5 | |||
| 2d87879af8 | |||
| d531fcb2c8 | |||
| f6a6ba2cd1 | |||
| e2e310cae5 | |||
| e3a9831ce1 | |||
| 0d977e5cca | |||
| 4bc3948fed | |||
| 6885771081 | |||
| 51c6ef904d | |||
| 42bd9f7c20 | |||
| cbc742b25a | |||
| 96758f1b6b | |||
| f2dcba0fe2 | |||
| 2f051c8da6 | |||
| b1f67d3daa | |||
| 8ca6405bc0 | |||
| 8f29f3578a | |||
| 2a3f489a2d | |||
| 6b150945fa | |||
| 03dbdd723d | |||
| ca769dca2d | |||
| 6f81384a06 | |||
| 4faf259aaa | |||
| 8cca5a64de | |||
| d3d3ba3586 | |||
| 6592b35755 | |||
| 112d0136c9 | |||
| fbf37e6861 | |||
| 528cdcafef |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
|
# Superpowers brainstorming/specs/plans — local only, never commit
|
||||||
|
docs/superpowers/
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
@ -45,3 +48,5 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
|||||||
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
43
docs/ARCHITECTURE.md
Normal file
43
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Application Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
L'application suit une **Clean Architecture allégée** en trois couches, avec une règle de
|
||||||
|
dépendance stricte : les dépendances pointent vers l'intérieur. Le state management est assuré
|
||||||
|
par Riverpod (providers manuels).
|
||||||
|
|
||||||
|
## Couches
|
||||||
|
|
||||||
|
### domain (Dart pur)
|
||||||
|
- **Entités** : `Pokemon`, immuable, sans dépendance Flutter/DB/API.
|
||||||
|
- **Repository (interface)** : `PokemonRepository` définit le contrat d'accès aux données.
|
||||||
|
- **Jeu** : `GameState` (état immuable) et `GameEngine` (règles pures, testables).
|
||||||
|
|
||||||
|
### data
|
||||||
|
- **DTO** : `PokemonDto` centralise tout le parsing JSON (API Tyradex + SQLite).
|
||||||
|
- **Datasources** : `PokemonLocalDataSource` (SQLite via sqflite), `PokemonRemoteDataSource` (HTTP).
|
||||||
|
- **Repository (impl)** : `PokemonRepositoryImpl` applique « DB locale d'abord, sinon API + cache ».
|
||||||
|
Sur le web, le datasource local est absent (`null`).
|
||||||
|
|
||||||
|
### presentation
|
||||||
|
- **Providers** : `pokemonRepositoryProvider` (DI), `pokedexProvider` (`AsyncNotifier`),
|
||||||
|
`gameProvider` (`Notifier<GameState>`), `selectedTabProvider` (onglet courant).
|
||||||
|
- **Pages** : `ConsumerWidget` / `ConsumerStatefulWidget` qui observent les providers.
|
||||||
|
- **Widgets** : éléments réutilisables (`PokemonImage`, `PokemonTile`, `PokemonTypeWidget`).
|
||||||
|
- **Thème** : `type_colors.dart` (couleur/format des types).
|
||||||
|
|
||||||
|
## Flux de données
|
||||||
|
|
||||||
|
```
|
||||||
|
UI (Consumer) → Notifier → GameEngine (règles) + Repository (données)
|
||||||
|
→ DataSource (SQLite / HTTP) → DTO → Entity
|
||||||
|
```
|
||||||
|
|
||||||
|
Riverpod re-render automatiquement les consommateurs concernés. Plus de bus d'événements global
|
||||||
|
ni d'accès inter-pages via l'arbre de widgets.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `test/domain/game_engine_test.dart` : règles du jeu.
|
||||||
|
- `test/data/pokemon_dto_test.dart` : parsing.
|
||||||
|
- `test/data/pokemon_repository_test.dart` : logique du repository (datasources factices).
|
||||||
28
docs/README.md
Normal file
28
docs/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Pokeguess
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokemon through a silhouette guessing game. The app fetch data from the Tyradex API and stores it locally for offline access.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- National Pokedex: Browse all 1025+ Pokemon from all generations.
|
||||||
|
- Guess Game: Identify Pokemon by their silhouette.
|
||||||
|
- Scoring System: Earn points for correct guesses, with bonuses for Shiny Pokemon. High scores are saved locally.
|
||||||
|
- Collection: Track caught and seen Pokemon.
|
||||||
|
- Search and Filter: Filter the collection by all or caught status and search by name.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Ensure Flutter SDK is installed.
|
||||||
|
2. Clone the repository.
|
||||||
|
3. Run `flutter pub get` to install dependencies.
|
||||||
|
4. Run `flutter run` to start the application.
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- Flutter: UI Framework.
|
||||||
|
- SQLite (sqflite): Local database.
|
||||||
|
- Tyradex API: Pokemon data source.
|
||||||
|
- Shared Preferences: High score persistence.
|
||||||
|
- Google Fonts: Custom typography.
|
||||||
@ -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>12.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -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, '12.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
|
||||||
|
|||||||
@ -1,27 +1,30 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- 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
|
||||||
@ -31,6 +34,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
|
|
||||||
@ -41,17 +45,20 @@ SPEC REPOS:
|
|||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
|
||||||
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
|
||||||
|
|
||||||
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
|
PODFILE CHECKSUM: 4b015915ec662986b54bf30ab778da63f7dda016
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@ -453,7 +453,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@ -580,7 +580,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@ -629,7 +629,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@ -54,6 +55,7 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import '../models/pokemon.dart';
|
|
||||||
import '../utils/pokemon_type.dart';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
// Classe qui permet de récupérer les données des pokémons depuis l'API Tyradex
|
|
||||||
// On utilise la librairie http pour effectuer les requêtes
|
|
||||||
// On utilise la librairie dart:convert pour convertir les données JSON en objet Dart
|
|
||||||
class PokemonApi {
|
|
||||||
static const String baseUrl = 'tyradex.vercel.app';
|
|
||||||
static const String pokemonUrl = 'api/v1/pokemon';
|
|
||||||
|
|
||||||
static Future<Pokemon> getPokemon(int id) async {
|
|
||||||
// On utilise la méthode get de la classe http pour effectuer une requête GET
|
|
||||||
// On utilise Uri.https pour construire l'URL de la requête
|
|
||||||
var response = await http.get(Uri.https(baseUrl, "$pokemonUrl/$id"));
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
// Si le code de retour de la requête n'est pas 200, on lève une exception
|
|
||||||
throw Exception('Erreur lors de la récupération du pokémon $id, code de retour ${response.statusCode}');
|
|
||||||
}
|
|
||||||
// On utilise la méthode jsonDecode de la librairie dart:convert pour convertir le corps de la réponse en fichier JSON
|
|
||||||
var json = jsonDecode(response.body);
|
|
||||||
// Récupération du nom en français
|
|
||||||
String name = json['name']['fr'] ?? json['name']['en'] ?? 'unknown';
|
|
||||||
|
|
||||||
// Récupération des types (en français dans l'API Tyradex)
|
|
||||||
List types = json['types'] ?? [];
|
|
||||||
PokemonType type1 = types.isNotEmpty
|
|
||||||
? frenchTypeToEnum(types[0]['name'])
|
|
||||||
: PokemonType.unknown;
|
|
||||||
PokemonType? type2 = types.length > 1
|
|
||||||
? frenchTypeToEnum(types[1]['name'])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// On crée un objet Pokemon à partir du fichier JSON
|
|
||||||
return Pokemon(
|
|
||||||
name: name,
|
|
||||||
id: id,
|
|
||||||
type1: type1,
|
|
||||||
type2: type2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../models/pokemon.dart';
|
|
||||||
|
|
||||||
// Widget qui permet d'afficher un pokémon
|
|
||||||
// Elle prend en paramètre un pokémon
|
|
||||||
// Elle affiche l'image du pokémon, son nom et son numéro
|
|
||||||
// Elle permet également de naviguer vers la page de détail du pokémon
|
|
||||||
class PokemonTile extends StatefulWidget {
|
|
||||||
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
final Pokemon pokemon;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PokemonTile> createState() => _PokemonTileState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PokemonTileState extends State<PokemonTile> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
// Lorsqu'on tap sur le widget, on navigue vers la page de détail du pokémon
|
|
||||||
// On utilise la méthode Navigator.pushNamed pour naviguer vers la page de détail
|
|
||||||
// On passe en paramètre du Navigator le contexte et la route de la page de détail
|
|
||||||
// on utilise "widget.pokemon" pour accéder au pokémon passé en paramètre; widget représente l'instance de la classe PokemonTile
|
|
||||||
Navigator.pushNamed(context, "/pokemon-detail", arguments: widget.pokemon);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: 150,
|
|
||||||
margin: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.black),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Image.network(widget.pokemon.imageUrl, height: 100),
|
|
||||||
Text(widget.pokemon.formatedName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
lib/core/config/app_constants.dart
Normal file
40
lib/core/config/app_constants.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/// Constantes globales de l'application, centralisées pour éviter les valeurs en dur.
|
||||||
|
class AppConstants {
|
||||||
|
AppConstants._();
|
||||||
|
|
||||||
|
/// Nombre total de Pokémon gérés (jusqu'à la Gen 9).
|
||||||
|
static const int totalPokemon = 1025;
|
||||||
|
|
||||||
|
/// Points gagnés pour une bonne réponse normale.
|
||||||
|
static const int pointsNormal = 10;
|
||||||
|
|
||||||
|
/// Points gagnés pour une bonne réponse sur un Pokémon shiny.
|
||||||
|
static const int pointsShiny = 20;
|
||||||
|
|
||||||
|
/// Nombre de vies au début d'une partie.
|
||||||
|
static const int startingLives = 3;
|
||||||
|
|
||||||
|
/// Nombre de skips au début d'une partie.
|
||||||
|
static const int startingSkips = 3;
|
||||||
|
|
||||||
|
/// Nombre d'indices au début d'une partie.
|
||||||
|
static const int startingHints = 3;
|
||||||
|
|
||||||
|
/// Une bonne réponse tous les N donne un indice bonus.
|
||||||
|
static const int hintBonusEvery = 5;
|
||||||
|
|
||||||
|
/// Une bonne réponse tous les N donne un skip bonus.
|
||||||
|
static const int skipBonusEvery = 10;
|
||||||
|
|
||||||
|
/// Probabilité d'apparition d'un shiny : 1 chance sur N.
|
||||||
|
static const int shinyOdds = 10;
|
||||||
|
|
||||||
|
/// Hôte de l'API Tyradex.
|
||||||
|
static const String apiBaseUrl = 'tyradex.app';
|
||||||
|
|
||||||
|
/// Chemin de l'endpoint Pokémon.
|
||||||
|
static const String apiPokemonPath = 'api/v1/pokemon';
|
||||||
|
|
||||||
|
/// Clé SharedPreferences pour le meilleur score.
|
||||||
|
static const String prefsBestScore = 'best_score';
|
||||||
|
}
|
||||||
21
lib/core/logger.dart
Normal file
21
lib/core/logger.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'dart:developer' as dev;
|
||||||
|
|
||||||
|
/// Logger minimal de l'application. Remplace les appels directs à print().
|
||||||
|
/// Silencieux en release (les blocs assert sont retirés du build release).
|
||||||
|
class AppLogger {
|
||||||
|
AppLogger._();
|
||||||
|
|
||||||
|
static void info(String message) {
|
||||||
|
assert(() {
|
||||||
|
dev.log(message, name: 'INFO');
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void error(String message, [Object? error, StackTrace? stackTrace]) {
|
||||||
|
assert(() {
|
||||||
|
dev.log(message, name: 'ERROR', error: error, stackTrace: stackTrace);
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/data/datasources/pokemon_local_datasource.dart
Normal file
69
lib/data/datasources/pokemon_local_datasource.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'package:sqflite_common/sqflite.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../dto/pokemon_dto.dart';
|
||||||
|
|
||||||
|
/// Accès SQLite local au Pokédex. Schéma et migrations identiques à l'ancien PokedexDatabase.
|
||||||
|
class PokemonLocalDataSource {
|
||||||
|
Future<Database>? _dbFuture;
|
||||||
|
|
||||||
|
Future<Database> _getDb() {
|
||||||
|
return _dbFuture ??= openDatabase(
|
||||||
|
'pokedex.db',
|
||||||
|
version: 2,
|
||||||
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
await db.execute('DROP TABLE IF EXISTS pokemon');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE TABLE pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCreate: (db, version) async {
|
||||||
|
await db.execute(
|
||||||
|
'CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
final db = await _getDb();
|
||||||
|
final rows = await db.query('pokemon');
|
||||||
|
return rows.map(PokemonDto.fromDb).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Pokemon?> getById(int id) async {
|
||||||
|
final db = await _getDb();
|
||||||
|
final rows = await db.query('pokemon', where: 'id = ?', whereArgs: [id]);
|
||||||
|
if (rows.isEmpty) return null;
|
||||||
|
return PokemonDto.fromDb(rows.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons) async {
|
||||||
|
final db = await _getDb();
|
||||||
|
final batch = db.batch();
|
||||||
|
for (final p in pokemons) {
|
||||||
|
batch.insert('pokemon', PokemonDto.toDb(p),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(Pokemon pokemon) async {
|
||||||
|
final db = await _getDb();
|
||||||
|
await db.update('pokemon', PokemonDto.toDb(pokemon),
|
||||||
|
where: 'id = ?', whereArgs: [pokemon.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> caughtCount() async {
|
||||||
|
final db = await _getDb();
|
||||||
|
final result =
|
||||||
|
await db.rawQuery('SELECT COUNT(*) FROM pokemon WHERE isCaught = 1');
|
||||||
|
return result.isNotEmpty ? (result.first.values.first as int? ?? 0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> seenCount() async {
|
||||||
|
final db = await _getDb();
|
||||||
|
final result =
|
||||||
|
await db.rawQuery('SELECT COUNT(*) FROM pokemon WHERE isSeen = 1');
|
||||||
|
return result.isNotEmpty ? (result.first.values.first as int? ?? 0) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
lib/data/datasources/pokemon_remote_datasource.dart
Normal file
46
lib/data/datasources/pokemon_remote_datasource.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../../core/logger.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../dto/pokemon_dto.dart';
|
||||||
|
|
||||||
|
/// Accès distant à l'API Tyradex.
|
||||||
|
class PokemonRemoteDataSource {
|
||||||
|
final http.Client _client;
|
||||||
|
|
||||||
|
PokemonRemoteDataSource({http.Client? client})
|
||||||
|
: _client = client ?? http.Client();
|
||||||
|
|
||||||
|
Future<Pokemon> getById(int id) async {
|
||||||
|
AppLogger.info('API: fetching Pokémon $id');
|
||||||
|
final response = await _client
|
||||||
|
.get(Uri.https(AppConstants.apiBaseUrl, '${AppConstants.apiPokemonPath}/$id'));
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
'Erreur récupération du pokémon $id, code ${response.statusCode}');
|
||||||
|
}
|
||||||
|
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return PokemonDto.fromTyradexJson(json, fallbackId: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
AppLogger.info('API: fetching ALL Pokémon');
|
||||||
|
final response = await _client
|
||||||
|
.get(Uri.https(AppConstants.apiBaseUrl, AppConstants.apiPokemonPath));
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to load pokemon (code ${response.statusCode})');
|
||||||
|
}
|
||||||
|
final List<dynamic> jsonList = jsonDecode(response.body);
|
||||||
|
final result = <Pokemon>[];
|
||||||
|
for (final json in jsonList) {
|
||||||
|
if (json['pokedex_id'] == 0) continue; // entrée générique Tyradex
|
||||||
|
try {
|
||||||
|
result.add(PokemonDto.fromTyradexJson(json as Map<String, dynamic>));
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/data/dto/pokemon_dto.dart
Normal file
101
lib/data/dto/pokemon_dto.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
|
||||||
|
/// Conversion JSON <-> entité Pokemon. Unique endroit de parsing.
|
||||||
|
class PokemonDto {
|
||||||
|
PokemonDto._();
|
||||||
|
|
||||||
|
/// Mappe un nom de type français (API Tyradex) vers l'enum.
|
||||||
|
static PokemonType frenchTypeToEnum(String frenchType) {
|
||||||
|
const map = {
|
||||||
|
'Normal': PokemonType.normal,
|
||||||
|
'Combat': PokemonType.fighting,
|
||||||
|
'Vol': PokemonType.flying,
|
||||||
|
'Poison': PokemonType.poison,
|
||||||
|
'Sol': PokemonType.ground,
|
||||||
|
'Roche': PokemonType.rock,
|
||||||
|
'Insecte': PokemonType.bug,
|
||||||
|
'Spectre': PokemonType.ghost,
|
||||||
|
'Acier': PokemonType.steel,
|
||||||
|
'Feu': PokemonType.fire,
|
||||||
|
'Eau': PokemonType.water,
|
||||||
|
'Plante': PokemonType.grass,
|
||||||
|
'Électrik': PokemonType.electric,
|
||||||
|
'Psy': PokemonType.psychic,
|
||||||
|
'Glace': PokemonType.ice,
|
||||||
|
'Dragon': PokemonType.dragon,
|
||||||
|
'Ténèbres': PokemonType.dark,
|
||||||
|
'Fée': PokemonType.fairy,
|
||||||
|
};
|
||||||
|
return map[frenchType] ?? PokemonType.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste).
|
||||||
|
/// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`.
|
||||||
|
static Pokemon fromTyradexJson(Map<String, dynamic> json, {int? fallbackId}) {
|
||||||
|
final id = (json['pokedex_id'] as int?) ??
|
||||||
|
fallbackId ??
|
||||||
|
(throw ArgumentError('pokedex_id absent et fallbackId non fourni'));
|
||||||
|
final nameMap = json['name'] as Map<String, dynamic>?;
|
||||||
|
final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown';
|
||||||
|
|
||||||
|
final List types = json['types'] ?? [];
|
||||||
|
final type1 =
|
||||||
|
types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown;
|
||||||
|
final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null;
|
||||||
|
|
||||||
|
final Map<String, dynamic>? stats = json['stats'];
|
||||||
|
return Pokemon(
|
||||||
|
name: name,
|
||||||
|
id: id,
|
||||||
|
type1: type1,
|
||||||
|
type2: type2,
|
||||||
|
hp: stats?['hp'] ?? 0,
|
||||||
|
atk: stats?['atk'] ?? 0,
|
||||||
|
def: stats?['def'] ?? 0,
|
||||||
|
spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex
|
||||||
|
description: json['category'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sérialise pour SQLite.
|
||||||
|
static Map<String, dynamic> toDb(Pokemon p) {
|
||||||
|
return {
|
||||||
|
'name': p.name,
|
||||||
|
'id': p.id,
|
||||||
|
'type1': p.type1.name,
|
||||||
|
'type2': p.type2?.name,
|
||||||
|
'hp': p.hp,
|
||||||
|
'atk': p.atk,
|
||||||
|
'def': p.def,
|
||||||
|
'spd': p.spd,
|
||||||
|
'description': p.description,
|
||||||
|
'isCaught': p.isCaught ? 1 : 0,
|
||||||
|
'isSeen': p.isSeen ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruit depuis une ligne SQLite.
|
||||||
|
static Pokemon fromDb(Map<String, dynamic> row) {
|
||||||
|
return Pokemon(
|
||||||
|
name: row['name'],
|
||||||
|
id: row['id'],
|
||||||
|
type1: PokemonType.values.firstWhere(
|
||||||
|
(e) => e.name == row['type1'],
|
||||||
|
orElse: () => PokemonType.unknown,
|
||||||
|
),
|
||||||
|
type2: row['type2'] != null
|
||||||
|
? PokemonType.values.firstWhere(
|
||||||
|
(e) => e.name == row['type2'],
|
||||||
|
orElse: () => PokemonType.unknown,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
hp: row['hp'] ?? 0,
|
||||||
|
atk: row['atk'] ?? 0,
|
||||||
|
def: row['def'] ?? 0,
|
||||||
|
spd: row['spd'] ?? 0,
|
||||||
|
description: row['description'],
|
||||||
|
isCaught: row['isCaught'] == 1 || row['isCaught'] == true,
|
||||||
|
isSeen: row['isSeen'] == 1 || row['isSeen'] == true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/data/repositories/pokemon_repository_impl.dart
Normal file
63
lib/data/repositories/pokemon_repository_impl.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../../core/logger.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../../domain/repositories/pokemon_repository.dart';
|
||||||
|
import '../datasources/pokemon_local_datasource.dart';
|
||||||
|
import '../datasources/pokemon_remote_datasource.dart';
|
||||||
|
|
||||||
|
/// Implémentation : DB locale d'abord, complétée/repliée sur l'API.
|
||||||
|
/// [local] vaut `null` sur le web (pas de SQLite).
|
||||||
|
class PokemonRepositoryImpl implements PokemonRepository {
|
||||||
|
final PokemonRemoteDataSource remote;
|
||||||
|
final PokemonLocalDataSource? local;
|
||||||
|
|
||||||
|
PokemonRepositoryImpl({required this.remote, this.local});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
final localDs = local;
|
||||||
|
if (localDs == null) return remote.getAll();
|
||||||
|
|
||||||
|
final cached = await localDs.getAll();
|
||||||
|
if (cached.length >= AppConstants.totalPokemon) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final remoteList = await remote.getAll();
|
||||||
|
await localDs.saveAll(remoteList);
|
||||||
|
return localDs.getAll();
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('Sync getAll échouée', e, st);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Pokemon?> getById(int id) async {
|
||||||
|
final localDs = local;
|
||||||
|
if (localDs != null) {
|
||||||
|
final cached = await localDs.getById(id);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final fetched = await remote.getById(id);
|
||||||
|
if (localDs != null) await localDs.saveAll([fetched]);
|
||||||
|
return fetched;
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('getById($id) échoué', e, st);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons) async =>
|
||||||
|
local?.saveAll(pokemons);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(Pokemon pokemon) async => local?.update(pokemon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> caughtCount() async => (await local?.caughtCount()) ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> seenCount() async => (await local?.seenCount()) ?? 0;
|
||||||
|
}
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import 'package:sqflite_common/sqflite.dart';
|
|
||||||
import '../models/pokemon.dart';
|
|
||||||
|
|
||||||
// Permet de gérer la base de données
|
|
||||||
class PokedexDatabase {
|
|
||||||
static Database? database;
|
|
||||||
static Future<void> initDatabase() async {
|
|
||||||
database = await openDatabase(
|
|
||||||
"pokedex.db", // Nom de la base de données
|
|
||||||
version: 1, // Version de la base de données, permet de gérer les migrations
|
|
||||||
onCreate: (db, version) async { // Fonction qui sera appelée lors de la création de la base de données
|
|
||||||
// Création de la table pokemon avec les colonnes id, name, type1 et type2
|
|
||||||
await db.execute("CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT)");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de récupérer la base de données
|
|
||||||
static Future<Database> getDatabase() async {
|
|
||||||
if (database == null) {
|
|
||||||
await initDatabase(); // On initialise la base de données si elle n'est pas encore initialisée
|
|
||||||
}
|
|
||||||
return database!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet d'insérer un Pokémon dans la base de données
|
|
||||||
static Future<void> insertPokemon(Pokemon pokemon) async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
await database.insert("pokemon", pokemon.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de récupérer la liste des pokémons dans la base de données
|
|
||||||
static Future<List<Pokemon>> getPokemonList() async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
var response = await database.query("pokemon");
|
|
||||||
return response.map((pokemon) => Pokemon.fromJson(pokemon)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de supprimer un Pokémon de la base de données
|
|
||||||
static Future<void> deletePokemon(int id) async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
await database.delete("pokemon", where: "id = ?", whereArgs: [id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de supprimer tous les pokémons de la base de données
|
|
||||||
static Future<void> deleteAllPokemon() async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
await database.delete("pokemon");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de mettre à jour un Pokémon dans la base de données
|
|
||||||
static Future<void> updatePokemon(Pokemon pokemon) async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
await database.update("pokemon", pokemon.toJson(), where: "id = ?", whereArgs: [pokemon.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de récupérer un Pokémon dans la base de données à partir de son ID
|
|
||||||
static Future<Pokemon?> getPokemon(int id) async {
|
|
||||||
Database database = await getDatabase();
|
|
||||||
List<Map<String, dynamic>> pokemonList = await database.query("pokemon", where: "id = ?", whereArgs: [id]);
|
|
||||||
if (pokemonList.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Pokemon.fromJson(pokemonList.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
lib/domain/entities/pokemon.dart
Normal file
71
lib/domain/entities/pokemon.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API.
|
||||||
|
class Pokemon {
|
||||||
|
final String name;
|
||||||
|
final int id;
|
||||||
|
final PokemonType type1;
|
||||||
|
final PokemonType? type2;
|
||||||
|
final int hp;
|
||||||
|
final int atk;
|
||||||
|
final int def;
|
||||||
|
final int spd;
|
||||||
|
final String? description;
|
||||||
|
final bool isCaught;
|
||||||
|
final bool isSeen;
|
||||||
|
|
||||||
|
const Pokemon({
|
||||||
|
required this.name,
|
||||||
|
required this.id,
|
||||||
|
required this.type1,
|
||||||
|
this.type2,
|
||||||
|
required this.hp,
|
||||||
|
required this.atk,
|
||||||
|
required this.def,
|
||||||
|
required this.spd,
|
||||||
|
this.description,
|
||||||
|
this.isCaught = false,
|
||||||
|
this.isSeen = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get imageUrl =>
|
||||||
|
'https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/$id/regular.png';
|
||||||
|
String get shinyImageUrl =>
|
||||||
|
'https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/$id/shiny.png';
|
||||||
|
String get cryUrl => 'https://pokemoncries.com/cries/$id.mp3';
|
||||||
|
|
||||||
|
String get formatedName =>
|
||||||
|
name.isEmpty ? name : name[0].toUpperCase() + name.substring(1);
|
||||||
|
|
||||||
|
Pokemon copyWith({
|
||||||
|
String? name,
|
||||||
|
int? id,
|
||||||
|
PokemonType? type1,
|
||||||
|
PokemonType? type2,
|
||||||
|
int? hp,
|
||||||
|
int? atk,
|
||||||
|
int? def,
|
||||||
|
int? spd,
|
||||||
|
String? description,
|
||||||
|
bool? isCaught,
|
||||||
|
bool? isSeen,
|
||||||
|
}) {
|
||||||
|
return Pokemon(
|
||||||
|
name: name ?? this.name,
|
||||||
|
id: id ?? this.id,
|
||||||
|
type1: type1 ?? this.type1,
|
||||||
|
type2: type2 ?? this.type2,
|
||||||
|
hp: hp ?? this.hp,
|
||||||
|
atk: atk ?? this.atk,
|
||||||
|
def: def ?? this.def,
|
||||||
|
spd: spd ?? this.spd,
|
||||||
|
description: description ?? this.description,
|
||||||
|
isCaught: isCaught ?? this.isCaught,
|
||||||
|
isSeen: isSeen ?? this.isSeen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Les différents types de Pokémon.
|
||||||
|
enum PokemonType {
|
||||||
|
normal, fighting, flying, poison, ground, rock, bug, ghost, steel, fire,
|
||||||
|
water, grass, electric, psychic, ice, dragon, dark, fairy, unknown, shadow
|
||||||
|
}
|
||||||
97
lib/domain/game/game_engine.dart
Normal file
97
lib/domain/game/game_engine.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../entities/pokemon.dart';
|
||||||
|
import 'game_state.dart';
|
||||||
|
|
||||||
|
/// Règles du jeu, pures (aucune IO, aucun widget). Entièrement testable.
|
||||||
|
class GameEngine {
|
||||||
|
const GameEngine();
|
||||||
|
|
||||||
|
/// Nouvelle partie : réinitialise tout sauf le bestScore.
|
||||||
|
GameState newGame(GameState s) {
|
||||||
|
return s.copyWith(
|
||||||
|
lives: AppConstants.startingLives,
|
||||||
|
skips: AppConstants.startingSkips,
|
||||||
|
hints: AppConstants.startingHints,
|
||||||
|
sessionCorrectCount: 0,
|
||||||
|
currentScore: 0,
|
||||||
|
status: GameStatus.loading,
|
||||||
|
isHintUsed: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Démarre une manche avec un nouveau Pokémon.
|
||||||
|
GameState startRound(GameState s, Pokemon p, {required bool isShiny}) {
|
||||||
|
return s.copyWith(
|
||||||
|
currentPokemon: p,
|
||||||
|
isShiny: isShiny,
|
||||||
|
isHintUsed: false,
|
||||||
|
status: GameStatus.playing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Soumet une réponse. Renvoie le nouvel état + la nature du résultat.
|
||||||
|
GuessOutcome submitGuess(GameState s, String guess) {
|
||||||
|
final pokemon = s.currentPokemon;
|
||||||
|
if (pokemon == null || s.status != GameStatus.playing) {
|
||||||
|
return GuessOutcome(s, GuessResult.invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedGuess = _normalize(guess.trim().toLowerCase());
|
||||||
|
final normalizedActual = _normalize(pokemon.name.toLowerCase());
|
||||||
|
|
||||||
|
if (normalizedGuess == normalizedActual) {
|
||||||
|
final newSession = s.sessionCorrectCount + 1;
|
||||||
|
final gained = s.isShiny ? AppConstants.pointsShiny : AppConstants.pointsNormal;
|
||||||
|
final newScore = s.currentScore + gained;
|
||||||
|
final hintBonus =
|
||||||
|
newSession % AppConstants.hintBonusEvery == 0 ? 1 : 0;
|
||||||
|
final skipBonus =
|
||||||
|
newSession % AppConstants.skipBonusEvery == 0 ? 1 : 0;
|
||||||
|
|
||||||
|
final newState = s.copyWith(
|
||||||
|
currentPokemon: pokemon.copyWith(isCaught: true, isSeen: true),
|
||||||
|
currentScore: newScore,
|
||||||
|
bestScore: newScore > s.bestScore ? newScore : s.bestScore,
|
||||||
|
sessionCorrectCount: newSession,
|
||||||
|
hints: s.hints + hintBonus,
|
||||||
|
skips: s.skips + skipBonus,
|
||||||
|
status: GameStatus.roundWon,
|
||||||
|
);
|
||||||
|
return GuessOutcome(newState, GuessResult.correct);
|
||||||
|
}
|
||||||
|
|
||||||
|
final remainingLives = s.lives - 1;
|
||||||
|
if (remainingLives <= 0) {
|
||||||
|
return GuessOutcome(
|
||||||
|
s.copyWith(lives: 0, status: GameStatus.gameOver),
|
||||||
|
GuessResult.gameOver,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GuessOutcome(
|
||||||
|
s.copyWith(lives: remainingLives),
|
||||||
|
GuessResult.wrong,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consomme un indice si disponible et non déjà utilisé.
|
||||||
|
GameState useHint(GameState s) {
|
||||||
|
if (s.isHintUsed || s.hints <= 0) return s;
|
||||||
|
return s.copyWith(isHintUsed: true, hints: s.hints - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consomme un skip si disponible.
|
||||||
|
GameState useSkip(GameState s) {
|
||||||
|
if (s.skips <= 0) return s;
|
||||||
|
return s.copyWith(skips: s.skips - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _normalize(String input) {
|
||||||
|
const withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
|
||||||
|
const withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
|
||||||
|
var output = input;
|
||||||
|
for (var i = 0; i < withDia.length; i++) {
|
||||||
|
output = output.replaceAll(withDia[i], withoutDia[i]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/domain/game/game_state.dart
Normal file
68
lib/domain/game/game_state.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../entities/pokemon.dart';
|
||||||
|
|
||||||
|
/// Phase courante de la partie.
|
||||||
|
enum GameStatus { loading, playing, roundWon, gameOver, error }
|
||||||
|
|
||||||
|
/// Résultat d'une soumission de réponse.
|
||||||
|
enum GuessResult { correct, wrong, gameOver, invalid }
|
||||||
|
|
||||||
|
/// État immuable d'une partie.
|
||||||
|
class GameState {
|
||||||
|
final Pokemon? currentPokemon;
|
||||||
|
final int lives;
|
||||||
|
final int skips;
|
||||||
|
final int hints;
|
||||||
|
final int sessionCorrectCount;
|
||||||
|
final int currentScore;
|
||||||
|
final int bestScore;
|
||||||
|
final bool isShiny;
|
||||||
|
final bool isHintUsed;
|
||||||
|
final GameStatus status;
|
||||||
|
|
||||||
|
const GameState({
|
||||||
|
this.currentPokemon,
|
||||||
|
this.lives = AppConstants.startingLives,
|
||||||
|
this.skips = AppConstants.startingSkips,
|
||||||
|
this.hints = AppConstants.startingHints,
|
||||||
|
this.sessionCorrectCount = 0,
|
||||||
|
this.currentScore = 0,
|
||||||
|
this.bestScore = 0,
|
||||||
|
this.isShiny = false,
|
||||||
|
this.isHintUsed = false,
|
||||||
|
this.status = GameStatus.loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
GameState copyWith({
|
||||||
|
Pokemon? currentPokemon,
|
||||||
|
int? lives,
|
||||||
|
int? skips,
|
||||||
|
int? hints,
|
||||||
|
int? sessionCorrectCount,
|
||||||
|
int? currentScore,
|
||||||
|
int? bestScore,
|
||||||
|
bool? isShiny,
|
||||||
|
bool? isHintUsed,
|
||||||
|
GameStatus? status,
|
||||||
|
}) {
|
||||||
|
return GameState(
|
||||||
|
currentPokemon: currentPokemon ?? this.currentPokemon,
|
||||||
|
lives: lives ?? this.lives,
|
||||||
|
skips: skips ?? this.skips,
|
||||||
|
hints: hints ?? this.hints,
|
||||||
|
sessionCorrectCount: sessionCorrectCount ?? this.sessionCorrectCount,
|
||||||
|
currentScore: currentScore ?? this.currentScore,
|
||||||
|
bestScore: bestScore ?? this.bestScore,
|
||||||
|
isShiny: isShiny ?? this.isShiny,
|
||||||
|
isHintUsed: isHintUsed ?? this.isHintUsed,
|
||||||
|
status: status ?? this.status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue d'un appel à GameEngine.submitGuess : nouvel état + nature du résultat.
|
||||||
|
class GuessOutcome {
|
||||||
|
final GameState state;
|
||||||
|
final GuessResult result;
|
||||||
|
const GuessOutcome(this.state, this.result);
|
||||||
|
}
|
||||||
22
lib/domain/repositories/pokemon_repository.dart
Normal file
22
lib/domain/repositories/pokemon_repository.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import '../entities/pokemon.dart';
|
||||||
|
|
||||||
|
/// Contrat d'accès aux données Pokémon. L'UI et le domaine ne connaissent que cette interface.
|
||||||
|
abstract interface class PokemonRepository {
|
||||||
|
/// Tous les Pokémon (DB locale d'abord, complétée par l'API si nécessaire).
|
||||||
|
Future<List<Pokemon>> getAll();
|
||||||
|
|
||||||
|
/// Un Pokémon par id (DB d'abord, sinon API + mise en cache). `null` si introuvable.
|
||||||
|
Future<Pokemon?> getById(int id);
|
||||||
|
|
||||||
|
/// Insère/remplace une liste de Pokémon.
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons);
|
||||||
|
|
||||||
|
/// Met à jour un Pokémon existant.
|
||||||
|
Future<void> update(Pokemon pokemon);
|
||||||
|
|
||||||
|
/// Nombre de Pokémon attrapés.
|
||||||
|
Future<int> caughtCount();
|
||||||
|
|
||||||
|
/// Nombre de Pokémon vus.
|
||||||
|
Future<int> seenCount();
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'pages/pokemon_list.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'pages/pokemon_detail.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
|
import 'presentation/pages/main_page.dart';
|
||||||
|
import 'presentation/pages/pokemon_detail.dart';
|
||||||
|
import 'presentation/pages/game_over_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -10,7 +13,7 @@ void main() {
|
|||||||
sqfliteFfiInit();
|
sqfliteFfiInit();
|
||||||
databaseFactory = databaseFactoryFfi;
|
databaseFactory = databaseFactoryFfi;
|
||||||
}
|
}
|
||||||
runApp(const MyApp());
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@ -19,18 +22,26 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Pokéguess', // Titre de l'application
|
title: 'Pokéguess',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: const Color(0xFFD32F2F),
|
||||||
|
surface: const Color(0xFF1B2333),
|
||||||
|
),
|
||||||
|
textTheme: GoogleFonts.vt323TextTheme(
|
||||||
|
Theme.of(context).textTheme,
|
||||||
|
).apply(
|
||||||
|
bodyColor: Colors.black87,
|
||||||
|
displayColor: Colors.black87,
|
||||||
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
debugShowCheckedModeBanner: false, // Permet de masquer la bannière "Debug"
|
debugShowCheckedModeBanner: false,
|
||||||
// home a été enlevé pour être remplacé par la route "/"
|
|
||||||
routes: {
|
routes: {
|
||||||
'/': (context) => const PokemonListPage(), // La route "/" est la page d'accueil
|
'/': (context) => const MainPage(),
|
||||||
'/pokemon-detail':(context) => const PokemonDetailPage(),
|
'/pokemon-detail': (context) => const PokemonDetailPage(),
|
||||||
}
|
'/game-over': (context) => const GameOverPage(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart' show kIsWeb; // Platform is not supported on web
|
|
||||||
import '../database/pokedex_database.dart';
|
|
||||||
import '../api/pokemon_api.dart';
|
|
||||||
import '../utils/pokemon_type.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// Classe représentant un Pokémon. Elle contient le nom, le numéro et les types du Pokémon.
|
|
||||||
// Elle contient aussi des propriétés calculées pour récupérer l'url de l'image du Pokémon, l'url de l'image shiny du Pokémon et l'url du cri du Pokémon.
|
|
||||||
class Pokemon {
|
|
||||||
String name;
|
|
||||||
int id;
|
|
||||||
PokemonType type1;
|
|
||||||
PokemonType? type2;
|
|
||||||
|
|
||||||
String get imageUrl => 'https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/$id/regular.png';
|
|
||||||
String get shinyImageUrl => 'https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/$id/shiny.png';
|
|
||||||
String get cryUrl => 'https://pokemoncries.com/cries/$id.mp3';
|
|
||||||
|
|
||||||
String get formatedName {
|
|
||||||
return name[0].toUpperCase() + name.substring(1);
|
|
||||||
}
|
|
||||||
Color get type1Color => typeToColor(type1);
|
|
||||||
Color get type2Color => typeToColor(type2 ?? PokemonType.unknown);
|
|
||||||
String get type1Formated => formatedTypeName(type1);
|
|
||||||
String get type2Formated => formatedTypeName(type2 ?? PokemonType.unknown);
|
|
||||||
|
|
||||||
Pokemon({
|
|
||||||
required this.name,
|
|
||||||
required this.id,
|
|
||||||
required this.type1,
|
|
||||||
this.type2, // Le type 2 n'est pas toujours présent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Constructeur qui permet de créer un Pokémon à partir d'un fichier JSON récupéré depuis l'API.
|
|
||||||
// Sera aussi utilisé pour la récupération depuis la base de données
|
|
||||||
factory Pokemon.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Pokemon(
|
|
||||||
name: json['name'],
|
|
||||||
id: json['id'],
|
|
||||||
// Parcours des valeurs de l'enum PokemonType et récupération de la première valeur qui correspond à la string 'PokemonType.${json['type1']}'
|
|
||||||
type1: PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.${json['type1']}'),
|
|
||||||
type2: json['type2'] != null ? PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.${json['type2']}') : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de convertir un Pokémon en fichier JSON. Sera aussi utilisé pour l'insertion dans la base de données
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'name': name,
|
|
||||||
'id': id,
|
|
||||||
'type1': type1.toString().split('.').last, // On récupère la valeur de l'enum PokemonType sans le préfixe 'PokemonType.'
|
|
||||||
'type2': type2?.toString().split('.').last,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode qui permet de récupérer un Pokémon à partir de son ID
|
|
||||||
// Si le Pokémon n'est pas présent dans la base de données, on le récupère depuis l'API
|
|
||||||
static Future<Pokemon?> fromID(int id) async {
|
|
||||||
Pokemon? pokemon;
|
|
||||||
if (!kIsWeb) {
|
|
||||||
// La base de données n'est pas disponible sur le web
|
|
||||||
pokemon = await PokedexDatabase.getPokemon(id);
|
|
||||||
}
|
|
||||||
if (pokemon == null) {
|
|
||||||
try {
|
|
||||||
pokemon = await PokemonApi.getPokemon(id);
|
|
||||||
if (!kIsWeb) {
|
|
||||||
// On insère le Pokémon dans la base de données
|
|
||||||
await PokedexDatabase.insertPokemon(pokemon);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pokemon;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum qui représente les différents types de Pokémon
|
|
||||||
enum PokemonType {
|
|
||||||
normal, fighting, flying, poison, ground, rock, bug, ghost, steel, fire, water, grass, electric, psychic, ice, dragon, dark, fairy, unknown, shadow
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
@override
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: " #${pokemon.id.toString().padLeft(4, "0")}",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
PokemonTypeWidget(pokemon.type1),
|
|
||||||
pokemon.type2 != null ? PokemonTypeWidget(pokemon.type2!) : Container(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PokemonListPage> createState() => _PokemonListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PokemonListPageState extends State<PokemonListPage> {
|
|
||||||
|
|
||||||
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(
|
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
future: Pokemon.fromID(index + 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Liste des pokémons'),
|
|
||||||
),
|
|
||||||
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
|
|
||||||
),
|
|
||||||
itemCount: 151, // On pourrait en mettre plus mais on va se limiter aux 151 premiers pokémons
|
|
||||||
itemBuilder: _buildPokemonTile,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
338
lib/presentation/pages/game_over_page.dart
Normal file
338
lib/presentation/pages/game_over_page.dart
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../providers/repository_provider.dart';
|
||||||
|
import '../widgets/pokemon_image.dart';
|
||||||
|
|
||||||
|
class GameOverPage extends ConsumerStatefulWidget {
|
||||||
|
const GameOverPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<GameOverPage> createState() => _GameOverPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameOverPageState extends ConsumerState<GameOverPage> {
|
||||||
|
int _seenCount = 0;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSeenCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSeenCount() async {
|
||||||
|
final count = await ref.read(pokemonRepositoryProvider).seenCount();
|
||||||
|
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;
|
||||||
|
final int score = args?['score'] ?? 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/${AppConstants.totalPokemon}",
|
||||||
|
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(
|
||||||
|
"SCORE",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"$score",
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
325
lib/presentation/pages/guess_page.dart
Normal file
325
lib/presentation/pages/guess_page.dart
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../../domain/game/game_state.dart';
|
||||||
|
import '../providers/game_provider.dart';
|
||||||
|
import '../providers/navigation_provider.dart';
|
||||||
|
import '../widgets/pokemon_image.dart';
|
||||||
|
|
||||||
|
class GuessPage extends ConsumerStatefulWidget {
|
||||||
|
const GuessPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<GuessPage> createState() => _GuessPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GuessPageState extends ConsumerState<GuessPage> {
|
||||||
|
final TextEditingController _guessController = TextEditingController();
|
||||||
|
bool _started = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Démarre la partie après le premier frame (le provider est prêt).
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!_started) {
|
||||||
|
_started = true;
|
||||||
|
ref.read(gameProvider.notifier).startNewGame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_guessController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onGuess() async {
|
||||||
|
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
|
||||||
|
if (!mounted) return;
|
||||||
|
final state = ref.read(gameProvider);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case GuessResult.correct:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(state.isShiny
|
||||||
|
? '✨ SHINY! You caught ${state.currentPokemon!.formatedName}! (+20 pts) ✨'
|
||||||
|
: 'Correct! You caught ${state.currentPokemon!.formatedName}!'),
|
||||||
|
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case GuessResult.wrong:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange));
|
||||||
|
break;
|
||||||
|
case GuessResult.gameOver:
|
||||||
|
await _showGameOver();
|
||||||
|
break;
|
||||||
|
case GuessResult.invalid:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_guessController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showGameOver() async {
|
||||||
|
final state = ref.read(gameProvider);
|
||||||
|
final playAgain = await Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
'/game-over',
|
||||||
|
arguments: {
|
||||||
|
'pokemonName': state.currentPokemon!.formatedName,
|
||||||
|
'score': state.currentScore,
|
||||||
|
'streak': state.sessionCorrectCount,
|
||||||
|
'pokemonImage': state.currentPokemon!.imageUrl,
|
||||||
|
},
|
||||||
|
) as bool?;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
if (playAgain == false) {
|
||||||
|
ref.read(selectedTabProvider.notifier).set(0); // onglet LIST
|
||||||
|
}
|
||||||
|
// true (Try Again), false (Back to Pokédex) et null (geste retour système)
|
||||||
|
// relancent tous une nouvelle partie pour ne pas rester bloqué en game over.
|
||||||
|
await ref.read(gameProvider.notifier).startNewGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _maskedName(String name) {
|
||||||
|
if (name.length <= 2) return name; // trop court pour masquer utilement
|
||||||
|
return '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = ref.watch(gameProvider);
|
||||||
|
|
||||||
|
if (state.status == GameStatus.loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (state.currentPokemon == null || state.status == GameStatus.error) {
|
||||||
|
return const Center(child: Text("Error loading Pokémon"));
|
||||||
|
}
|
||||||
|
|
||||||
|
final pokemon = state.currentPokemon!;
|
||||||
|
final isGuessed = state.status == GameStatus.roundWon;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 100,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) => Container(
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
color: Colors.black.withAlpha(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Silhouette screen
|
||||||
|
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: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
||||||
|
fallbackUrl: pokemon.imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
: PokemonImage(
|
||||||
|
imageUrl: state.isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
|
||||||
|
fallbackUrl: pokemon.imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
color: state.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(
|
||||||
|
state.isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: state.isShiny ? Colors.yellow[400] : Colors.white,
|
||||||
|
fontSize: state.isShiny ? 18 : 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Lives
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(AppConstants.startingLives, (index) {
|
||||||
|
return Icon(
|
||||||
|
index < state.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 (state.isHintUsed)
|
||||||
|
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: ${_maskedName(pokemon.formatedName)}",
|
||||||
|
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: (_) => _onGuess(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (isGuessed)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 60,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => ref.read(gameProvider.notifier).loadNextPokemon(),
|
||||||
|
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: _onGuess,
|
||||||
|
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: (state.isHintUsed || state.hints <= 0)
|
||||||
|
? null
|
||||||
|
: () => ref.read(gameProvider.notifier).useHint(),
|
||||||
|
icon: const Icon(Icons.lightbulb, color: Colors.black87),
|
||||||
|
label: Text("HINT (${state.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: state.skips > 0
|
||||||
|
? () => ref.read(gameProvider.notifier).useSkip()
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.skip_next, color: Colors.black87),
|
||||||
|
label: Text("SKIP (${state.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
|
||||||
|
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("${state.currentScore}",
|
||||||
|
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3))),
|
||||||
|
const Divider(height: 24),
|
||||||
|
Text("PERSONAL BEST: ${state.bestScore}",
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/presentation/pages/main_page.dart
Normal file
61
lib/presentation/pages/main_page.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../providers/navigation_provider.dart';
|
||||||
|
import 'pokemon_list.dart';
|
||||||
|
import 'guess_page.dart';
|
||||||
|
|
||||||
|
class MainPage extends ConsumerWidget {
|
||||||
|
const MainPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const List<Widget> _pages = [
|
||||||
|
PokemonListPage(),
|
||||||
|
GuessPage(),
|
||||||
|
Center(child: Text("SYSTEM PAGE placeholder")),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentIndex = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
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: 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) => ref.read(selectedTabProvider.notifier).set(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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
287
lib/presentation/pages/pokemon_detail.dart
Normal file
287
lib/presentation/pages/pokemon_detail.dart
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../widgets/pokemon_type.dart';
|
||||||
|
import '../widgets/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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
169
lib/presentation/pages/pokemon_list.dart
Normal file
169
lib/presentation/pages/pokemon_list.dart
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../providers/pokedex_provider.dart';
|
||||||
|
import '../widgets/pokemon_tile.dart';
|
||||||
|
|
||||||
|
class PokemonListPage extends ConsumerStatefulWidget {
|
||||||
|
const PokemonListPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PokemonListPage> createState() => _PokemonListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PokemonListPageState extends ConsumerState<PokemonListPage> {
|
||||||
|
String _filter = 'ALL'; // ALL, CAUGHT
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pokemon> _applyFilter(List<Pokemon> all) {
|
||||||
|
if (_filter == 'CAUGHT') return all.where((p) => p.isCaught).toList();
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final pokedexAsync = ref.watch(pokedexProvider);
|
||||||
|
final caughtCount = ref.watch(caughtCountProvider);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
|
||||||
|
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')} / ${pokedexAsync.valueOrNull?.length ?? 0}',
|
||||||
|
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: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 100,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) => Container(
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
color: Colors.black.withAlpha(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pokedexAsync.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => Center(
|
||||||
|
child: Text('Erreur de chargement\n$e',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.black54)),
|
||||||
|
),
|
||||||
|
data: (all) {
|
||||||
|
final filtered = _applyFilter(all);
|
||||||
|
if (filtered.isEmpty) {
|
||||||
|
return 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: filtered.length,
|
||||||
|
itemBuilder: (context, index) => PokemonTile(filtered[index]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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) {
|
||||||
|
setState(() => _filter = title);
|
||||||
|
if (_scrollController.hasClients) _scrollController.jumpTo(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/presentation/providers/game_provider.dart
Normal file
83
lib/presentation/providers/game_provider.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../../domain/game/game_engine.dart';
|
||||||
|
import '../../domain/game/game_state.dart';
|
||||||
|
import '../../core/logger.dart';
|
||||||
|
import 'pokedex_provider.dart';
|
||||||
|
import 'repository_provider.dart';
|
||||||
|
|
||||||
|
/// Orchestration de la partie : relie GameEngine (règles), le repository (données)
|
||||||
|
/// et SharedPreferences (best score).
|
||||||
|
class GameNotifier extends Notifier<GameState> {
|
||||||
|
static const _engine = GameEngine();
|
||||||
|
final _random = Random();
|
||||||
|
|
||||||
|
@override
|
||||||
|
GameState build() => const GameState();
|
||||||
|
|
||||||
|
Future<void> startNewGame() async {
|
||||||
|
state = _engine.newGame(state);
|
||||||
|
await _loadBestScore();
|
||||||
|
await loadNextPokemon();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadBestScore() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
state = state.copyWith(bestScore: prefs.getInt(AppConstants.prefsBestScore) ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadNextPokemon() async {
|
||||||
|
state = state.copyWith(status: GameStatus.loading);
|
||||||
|
final repo = ref.read(pokemonRepositoryProvider);
|
||||||
|
final isShiny = _random.nextInt(AppConstants.shinyOdds) == 0;
|
||||||
|
|
||||||
|
final id = _random.nextInt(AppConstants.totalPokemon) + 1;
|
||||||
|
try {
|
||||||
|
final pokemon = await repo.getById(id);
|
||||||
|
if (pokemon == null) {
|
||||||
|
state = state.copyWith(status: GameStatus.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = _engine.startRound(state, pokemon, isShiny: isShiny);
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('loadNextPokemon a échoué', e, st);
|
||||||
|
state = state.copyWith(status: GameStatus.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GuessResult> submitGuess(String guess) async {
|
||||||
|
final outcome = _engine.submitGuess(state, guess);
|
||||||
|
state = outcome.state;
|
||||||
|
|
||||||
|
if (outcome.result == GuessResult.correct) {
|
||||||
|
final repo = ref.read(pokemonRepositoryProvider);
|
||||||
|
final caught = state.currentPokemon;
|
||||||
|
if (caught != null) await repo.update(caught);
|
||||||
|
await _persistBestScore();
|
||||||
|
if (!kIsWeb) ref.invalidate(pokedexProvider); // rafraîchit la liste (pas de persistance sur web)
|
||||||
|
}
|
||||||
|
return outcome.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistBestScore() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final stored = prefs.getInt(AppConstants.prefsBestScore) ?? 0;
|
||||||
|
if (state.bestScore > stored) {
|
||||||
|
await prefs.setInt(AppConstants.prefsBestScore, state.bestScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void useHint() => state = _engine.useHint(state);
|
||||||
|
|
||||||
|
Future<void> useSkip() async {
|
||||||
|
final updated = _engine.useSkip(state);
|
||||||
|
if (updated.skips == state.skips) return; // pas de skip dispo
|
||||||
|
state = updated;
|
||||||
|
await loadNextPokemon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final gameProvider = NotifierProvider<GameNotifier, GameState>(GameNotifier.new);
|
||||||
13
lib/presentation/providers/navigation_provider.dart
Normal file
13
lib/presentation/providers/navigation_provider.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
/// Index de l'onglet sélectionné dans la navigation principale.
|
||||||
|
/// Remplace le hack findAncestorStateOfType<MainPageState>.
|
||||||
|
class SelectedTabNotifier extends Notifier<int> {
|
||||||
|
@override
|
||||||
|
int build() => 0;
|
||||||
|
|
||||||
|
void set(int index) => state = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedTabProvider =
|
||||||
|
NotifierProvider<SelectedTabNotifier, int>(SelectedTabNotifier.new);
|
||||||
34
lib/presentation/providers/pokedex_provider.dart
Normal file
34
lib/presentation/providers/pokedex_provider.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import 'repository_provider.dart';
|
||||||
|
|
||||||
|
/// Liste complète du Pokédex (triée par id), avec synchro initiale gérée par le repository.
|
||||||
|
class PokedexNotifier extends AsyncNotifier<List<Pokemon>> {
|
||||||
|
Future<List<Pokemon>> _load() async {
|
||||||
|
final repo = ref.read(pokemonRepositoryProvider);
|
||||||
|
final list = await repo.getAll();
|
||||||
|
list.sort((a, b) => a.id.compareTo(b.id));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> build() => _load();
|
||||||
|
|
||||||
|
/// Recharge la liste (ex. après avoir attrapé un Pokémon).
|
||||||
|
Future<void> refresh() async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(_load);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final pokedexProvider =
|
||||||
|
AsyncNotifierProvider<PokedexNotifier, List<Pokemon>>(PokedexNotifier.new);
|
||||||
|
|
||||||
|
/// Nombre de Pokémon attrapés, dérivé de la liste.
|
||||||
|
final caughtCountProvider = Provider<int>((ref) {
|
||||||
|
final async = ref.watch(pokedexProvider);
|
||||||
|
return async.maybeWhen(
|
||||||
|
data: (list) => list.where((p) => p.isCaught).length,
|
||||||
|
orElse: () => 0,
|
||||||
|
);
|
||||||
|
});
|
||||||
13
lib/presentation/providers/repository_provider.dart
Normal file
13
lib/presentation/providers/repository_provider.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../data/datasources/pokemon_local_datasource.dart';
|
||||||
|
import '../../data/datasources/pokemon_remote_datasource.dart';
|
||||||
|
import '../../data/repositories/pokemon_repository_impl.dart';
|
||||||
|
import '../../domain/repositories/pokemon_repository.dart';
|
||||||
|
|
||||||
|
/// Point d'injection unique du repository. Sur le web, pas de SQLite (local = null).
|
||||||
|
final pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
|
||||||
|
final remote = PokemonRemoteDataSource();
|
||||||
|
final local = kIsWeb ? null : PokemonLocalDataSource();
|
||||||
|
return PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
});
|
||||||
35
lib/presentation/theme/type_colors.dart
Normal file
35
lib/presentation/theme/type_colors.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
|
||||||
|
/// Couleur d'affichage associée à un type de Pokémon.
|
||||||
|
Color typeToColor(PokemonType type) {
|
||||||
|
final Map<PokemonType, Color> map = {
|
||||||
|
PokemonType.normal: Colors.white,
|
||||||
|
PokemonType.fire: Colors.red,
|
||||||
|
PokemonType.water: Colors.blue,
|
||||||
|
PokemonType.electric: Colors.yellow,
|
||||||
|
PokemonType.grass: Colors.green,
|
||||||
|
PokemonType.ice: Colors.cyan,
|
||||||
|
PokemonType.fighting: Colors.orange,
|
||||||
|
PokemonType.poison: Colors.purple,
|
||||||
|
PokemonType.ground: Colors.brown,
|
||||||
|
PokemonType.flying: Colors.indigo,
|
||||||
|
PokemonType.psychic: Colors.pink,
|
||||||
|
PokemonType.bug: Colors.lightGreen,
|
||||||
|
PokemonType.rock: Colors.grey,
|
||||||
|
PokemonType.ghost: Colors.indigo,
|
||||||
|
PokemonType.dragon: Colors.indigo,
|
||||||
|
PokemonType.dark: Colors.black45,
|
||||||
|
PokemonType.steel: Colors.grey.shade600,
|
||||||
|
PokemonType.fairy: Colors.pinkAccent,
|
||||||
|
PokemonType.unknown: Colors.transparent,
|
||||||
|
PokemonType.shadow: Colors.transparent,
|
||||||
|
};
|
||||||
|
return map[type] ?? Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nom du type avec une majuscule initiale.
|
||||||
|
String formatedTypeName(PokemonType type) {
|
||||||
|
final typeName = type.name;
|
||||||
|
return typeName[0].toUpperCase() + typeName.substring(1);
|
||||||
|
}
|
||||||
87
lib/presentation/widgets/pokemon_image.dart
Normal file
87
lib/presentation/widgets/pokemon_image.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/presentation/widgets/pokemon_tile.dart
Normal file
87
lib/presentation/widgets/pokemon_tile.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import 'pokemon_image.dart';
|
||||||
|
|
||||||
|
class PokemonTile extends StatelessWidget {
|
||||||
|
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
final Pokemon pokemon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If not caught, we don't allow navigating to the detail page (to force guessing)
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: pokemon.isCaught ? () {
|
||||||
|
Navigator.pushNamed(context, "/pokemon-detail", arguments: pokemon);
|
||||||
|
} : null,
|
||||||
|
child: Container(
|
||||||
|
height: 80,
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE2EBF0), // lighter grey for tile surface
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(25),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(2, 2),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Image box
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: pokemon.isCaught ? const Color(0xFF78909C) : Colors.grey[700],
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: pokemon.isCaught
|
||||||
|
? PokemonImage(imageUrl: pokemon.imageUrl, fit: BoxFit.contain)
|
||||||
|
: const SizedBox.expand(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
// Name texts
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'No. ${pokemon.id.toString().padLeft(3, '0')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
pokemon.isCaught ? pokemon.formatedName.toUpperCase() : '???',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: pokemon.isCaught ? Colors.black87 : Colors.grey[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Caught check icon
|
||||||
|
if (pokemon.isCaught)
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 28)
|
||||||
|
else
|
||||||
|
Icon(Icons.help, color: Colors.grey[400], size: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/pokemon.dart';
|
import '../../domain/entities/pokemon.dart';
|
||||||
import '../utils/pokemon_type.dart';
|
import '../theme/type_colors.dart';
|
||||||
|
|
||||||
// Widget qui permet d'afficher un type de Pokémon
|
// Widget qui permet d'afficher un type de Pokémon
|
||||||
// Elle prend en paramètre un type de Pokémon
|
// Elle prend en paramètre un type de Pokémon
|
||||||
@ -16,8 +16,8 @@ class PokemonTypeWidget extends StatelessWidget {
|
|||||||
Color typeColor = typeToColor(type);
|
Color typeColor = typeToColor(type);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
margin: const EdgeInsets.only(right: 10),
|
margin: const EdgeInsets.only(right: 6),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: typeColor,
|
color: typeColor,
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../models/pokemon.dart';
|
|
||||||
|
|
||||||
// Convertit un nom de type français (de l'API Tyradex) en PokemonType
|
|
||||||
PokemonType frenchTypeToEnum(String frenchType) {
|
|
||||||
const Map<String, PokemonType> frenchToEnglish = {
|
|
||||||
'Normal': PokemonType.normal,
|
|
||||||
'Combat': PokemonType.fighting,
|
|
||||||
'Vol': PokemonType.flying,
|
|
||||||
'Poison': PokemonType.poison,
|
|
||||||
'Sol': PokemonType.ground,
|
|
||||||
'Roche': PokemonType.rock,
|
|
||||||
'Insecte': PokemonType.bug,
|
|
||||||
'Spectre': PokemonType.ghost,
|
|
||||||
'Acier': PokemonType.steel,
|
|
||||||
'Feu': PokemonType.fire,
|
|
||||||
'Eau': PokemonType.water,
|
|
||||||
'Plante': PokemonType.grass,
|
|
||||||
'Électrik': PokemonType.electric,
|
|
||||||
'Psy': PokemonType.psychic,
|
|
||||||
'Glace': PokemonType.ice,
|
|
||||||
'Dragon': PokemonType.dragon,
|
|
||||||
'Ténèbres': PokemonType.dark,
|
|
||||||
'Fée': PokemonType.fairy,
|
|
||||||
};
|
|
||||||
return frenchToEnglish[frenchType] ?? PokemonType.unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permet de mapper un type de Pokémon avec une couleur
|
|
||||||
Color typeToColor(PokemonType type) {
|
|
||||||
Map<PokemonType, Color> typeToColor = {
|
|
||||||
PokemonType.normal: Colors.white,
|
|
||||||
PokemonType.fire: Colors.red,
|
|
||||||
PokemonType.water: Colors.blue,
|
|
||||||
PokemonType.electric: Colors.yellow,
|
|
||||||
PokemonType.grass: Colors.green,
|
|
||||||
PokemonType.ice: Colors.cyan,
|
|
||||||
PokemonType.fighting: Colors.orange,
|
|
||||||
PokemonType.poison: Colors.purple,
|
|
||||||
PokemonType.ground: Colors.brown,
|
|
||||||
PokemonType.flying: Colors.indigo,
|
|
||||||
PokemonType.psychic: Colors.pink,
|
|
||||||
PokemonType.bug: Colors.lightGreen,
|
|
||||||
PokemonType.rock: Colors.grey,
|
|
||||||
PokemonType.ghost: Colors.indigo,
|
|
||||||
PokemonType.dragon: Colors.indigo,
|
|
||||||
PokemonType.dark: Colors.black45,
|
|
||||||
PokemonType.steel: Colors.grey.shade600,
|
|
||||||
PokemonType.fairy: Colors.pinkAccent,
|
|
||||||
PokemonType.unknown: Colors.transparent,
|
|
||||||
PokemonType.shadow: Colors.transparent,
|
|
||||||
};
|
|
||||||
return typeToColor[type] ?? Colors.transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Met le nom du type de Pokémon avec une majuscule
|
|
||||||
String formatedTypeName(PokemonType type) {
|
|
||||||
String typeName = type.toString().split('.').last.replaceAll('PokemonType.', '');
|
|
||||||
return typeName[0].toUpperCase() + typeName.substring(1);
|
|
||||||
}
|
|
||||||
@ -5,10 +5,12 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import sqlite3_flutter_libs
|
import sqlite3_flutter_libs
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
platform :osx, '10.14'
|
platform :osx, '10.15'
|
||||||
|
|
||||||
# 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'
|
||||||
|
|||||||
64
macos/Podfile.lock
Normal file
64
macos/Podfile.lock
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
PODS:
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- sqflite_darwin (0.0.4):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- sqlite3 (3.51.1):
|
||||||
|
- sqlite3/common (= 3.51.1)
|
||||||
|
- sqlite3/common (3.51.1)
|
||||||
|
- sqlite3/dbstatvtab (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3/fts5 (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3/math (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3/perf-threadsafe (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3/rtree (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3/session (3.51.1):
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- sqlite3 (~> 3.51.1)
|
||||||
|
- sqlite3/dbstatvtab
|
||||||
|
- sqlite3/fts5
|
||||||
|
- sqlite3/math
|
||||||
|
- sqlite3/perf-threadsafe
|
||||||
|
- sqlite3/rtree
|
||||||
|
- sqlite3/session
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
|
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- sqlite3
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
sqflite_darwin:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
||||||
|
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@ -21,12 +21,14 @@
|
|||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
0FDCA1A045353300B1A8E8E8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76F7019D0D88279494D5531D /* Pods_Runner.framework */; };
|
||||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
851351654985DAD48988C209 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -60,11 +62,12 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
0B6A0CC857D58D26A0B79C8D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
33CC10ED2044A3C60003C045 /* pokedex.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "pokedex.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
33CC10ED2044A3C60003C045 /* pokedex.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pokedex.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
@ -76,8 +79,15 @@
|
|||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
|
40D2AEC0C927C89AC54D3DB4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
695FFFED6DD87814A538DFCF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
7370BBE38ED573ED71B9080C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
76F7019D0D88279494D5531D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
C2B4FF7F883B72793F5DD3F2 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
D5F1F019BFDE6F413FB421E0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -85,6 +95,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
851351654985DAD48988C209 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -92,6 +103,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
0FDCA1A045353300B1A8E8E8 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -125,6 +137,7 @@
|
|||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
|
62DC207A1894FA0B7B1EACF9 /* Pods */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -172,9 +185,25 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
62DC207A1894FA0B7B1EACF9 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D5F1F019BFDE6F413FB421E0 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
0B6A0CC857D58D26A0B79C8D /* Pods-Runner.release.xcconfig */,
|
||||||
|
40D2AEC0C927C89AC54D3DB4 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
C2B4FF7F883B72793F5DD3F2 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
695FFFED6DD87814A538DFCF /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
7370BBE38ED573ED71B9080C /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
76F7019D0D88279494D5531D /* Pods_Runner.framework */,
|
||||||
|
4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -186,6 +215,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
91CA1F8B03DD046BE3FEDD74 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
@ -204,11 +234,13 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
EBA006E33EB355F2C0B61BDE /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
45EA0EFB7B0E13C201870C85 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -227,7 +259,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
331C80D4294CF70F00263BE5 = {
|
331C80D4294CF70F00263BE5 = {
|
||||||
@ -328,6 +360,67 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
};
|
};
|
||||||
|
45EA0EFB7B0E13C201870C85 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
91CA1F8B03DD046BE3FEDD74 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
EBA006E33EB355F2C0B61BDE /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -379,6 +472,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = C2B4FF7F883B72793F5DD3F2 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@ -393,6 +487,7 @@
|
|||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 695FFFED6DD87814A538DFCF /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@ -407,6 +502,7 @@
|
|||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7370BBE38ED573ED71B9080C /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@ -457,7 +553,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@ -536,7 +632,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@ -583,7 +679,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@ -59,6 +59,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
|
||||||
@NSApplicationMain
|
@main
|
||||||
class AppDelegate: FlutterAppDelegate {
|
class AppDelegate: FlutterAppDelegate {
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
|
|||||||
@ -4,5 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -24,6 +24,9 @@ dependencies:
|
|||||||
|
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
google_fonts: ^8.0.2
|
||||||
|
shared_preferences: ^2.5.4
|
||||||
|
flutter_riverpod: ^2.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
8
quel-est-ce-pokemon.code-workspace
Normal file
8
quel-est-ce-pokemon.code-workspace
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
83
test/data/pokemon_dto_test.dart
Normal file
83
test/data/pokemon_dto_test.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pokeguess/domain/entities/pokemon.dart';
|
||||||
|
import 'package:pokeguess/data/dto/pokemon_dto.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PokemonDto.fromTyradexJson', () {
|
||||||
|
test('parse un Pokémon complet avec deux types', () {
|
||||||
|
final json = {
|
||||||
|
'pokedex_id': 6,
|
||||||
|
'name': {'fr': 'Dracaufeu', 'en': 'Charizard'},
|
||||||
|
'types': [
|
||||||
|
{'name': 'Feu'},
|
||||||
|
{'name': 'Vol'},
|
||||||
|
],
|
||||||
|
'stats': {'hp': 78, 'atk': 84, 'def': 78, 'vit': 100},
|
||||||
|
'category': 'Pokémon Flamme',
|
||||||
|
};
|
||||||
|
final p = PokemonDto.fromTyradexJson(json);
|
||||||
|
expect(p.id, 6);
|
||||||
|
expect(p.name, 'Dracaufeu');
|
||||||
|
expect(p.type1, PokemonType.fire);
|
||||||
|
expect(p.type2, PokemonType.flying);
|
||||||
|
expect(p.hp, 78);
|
||||||
|
expect(p.spd, 100);
|
||||||
|
expect(p.description, 'Pokémon Flamme');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('utilise fallbackId quand pokedex_id absent', () {
|
||||||
|
final json = {
|
||||||
|
'name': {'fr': 'Bulbizarre'},
|
||||||
|
'types': [{'name': 'Plante'}],
|
||||||
|
'stats': {'hp': 45, 'atk': 49, 'def': 49, 'vit': 45},
|
||||||
|
'category': 'Pokémon Graine',
|
||||||
|
};
|
||||||
|
final p = PokemonDto.fromTyradexJson(json, fallbackId: 1);
|
||||||
|
expect(p.id, 1);
|
||||||
|
expect(p.type2, isNull);
|
||||||
|
expect(p.type1, PokemonType.grass);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type inconnu mappé sur PokemonType.unknown', () {
|
||||||
|
final json = {
|
||||||
|
'pokedex_id': 999,
|
||||||
|
'name': {'fr': 'Test'},
|
||||||
|
'types': [{'name': 'TypeInexistant'}],
|
||||||
|
'stats': {'hp': 1, 'atk': 1, 'def': 1, 'vit': 1},
|
||||||
|
};
|
||||||
|
final p = PokemonDto.fromTyradexJson(json);
|
||||||
|
expect(p.type1, PokemonType.unknown);
|
||||||
|
expect(p.description, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('PokemonDto round-trip DB', () {
|
||||||
|
test('toDb puis fromDb reconstruit le Pokémon', () {
|
||||||
|
const original = Pokemon(
|
||||||
|
name: 'pikachu',
|
||||||
|
id: 25,
|
||||||
|
type1: PokemonType.electric,
|
||||||
|
type2: null,
|
||||||
|
hp: 35,
|
||||||
|
atk: 55,
|
||||||
|
def: 40,
|
||||||
|
spd: 90,
|
||||||
|
description: 'Souris',
|
||||||
|
isCaught: true,
|
||||||
|
isSeen: true,
|
||||||
|
);
|
||||||
|
final row = PokemonDto.toDb(original);
|
||||||
|
expect(row['type1'], 'electric');
|
||||||
|
expect(row['type2'], isNull);
|
||||||
|
expect(row['isCaught'], 1);
|
||||||
|
|
||||||
|
final restored = PokemonDto.fromDb(row);
|
||||||
|
expect(restored.name, 'pikachu');
|
||||||
|
expect(restored.id, 25);
|
||||||
|
expect(restored.type1, PokemonType.electric);
|
||||||
|
expect(restored.type2, isNull);
|
||||||
|
expect(restored.isCaught, true);
|
||||||
|
expect(restored.isSeen, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
127
test/data/pokemon_repository_test.dart
Normal file
127
test/data/pokemon_repository_test.dart
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pokeguess/domain/entities/pokemon.dart';
|
||||||
|
import 'package:pokeguess/data/datasources/pokemon_local_datasource.dart';
|
||||||
|
import 'package:pokeguess/data/datasources/pokemon_remote_datasource.dart';
|
||||||
|
import 'package:pokeguess/data/repositories/pokemon_repository_impl.dart';
|
||||||
|
|
||||||
|
Pokemon _mk(int id, {bool caught = false}) => Pokemon(
|
||||||
|
name: 'p$id',
|
||||||
|
id: id,
|
||||||
|
type1: PokemonType.normal,
|
||||||
|
hp: 1, atk: 1, def: 1, spd: 1,
|
||||||
|
isCaught: caught,
|
||||||
|
);
|
||||||
|
|
||||||
|
class FakeLocal implements PokemonLocalDataSource {
|
||||||
|
final Map<int, Pokemon> store;
|
||||||
|
FakeLocal([Map<int, Pokemon>? init]) : store = init ?? {};
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async => store.values.toList();
|
||||||
|
@override
|
||||||
|
Future<Pokemon?> getById(int id) async => store[id];
|
||||||
|
@override
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons) async {
|
||||||
|
for (final p in pokemons) { store[p.id] = p; }
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Future<void> update(Pokemon pokemon) async => store[pokemon.id] = pokemon;
|
||||||
|
@override
|
||||||
|
Future<int> caughtCount() async =>
|
||||||
|
store.values.where((p) => p.isCaught).length;
|
||||||
|
@override
|
||||||
|
Future<int> seenCount() async => store.values.where((p) => p.isSeen).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeRemote implements PokemonRemoteDataSource {
|
||||||
|
final List<Pokemon> all;
|
||||||
|
int getAllCalls = 0;
|
||||||
|
int getByIdCalls = 0;
|
||||||
|
FakeRemote(this.all);
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
getAllCalls++;
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Future<Pokemon> getById(int id) async {
|
||||||
|
getByIdCalls++;
|
||||||
|
return all.firstWhere((p) => p.id == id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThrowingRemote implements PokemonRemoteDataSource {
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async => throw Exception('network');
|
||||||
|
@override
|
||||||
|
Future<Pokemon> getById(int id) async => throw Exception('network');
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('getById', () {
|
||||||
|
test('retourne le cache local sans appeler l\'API', () async {
|
||||||
|
final local = FakeLocal({5: _mk(5)});
|
||||||
|
final remote = FakeRemote([_mk(5)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final p = await repo.getById(5);
|
||||||
|
expect(p?.id, 5);
|
||||||
|
expect(remote.getByIdCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('va chercher sur l\'API et met en cache si absent en local', () async {
|
||||||
|
final local = FakeLocal();
|
||||||
|
final remote = FakeRemote([_mk(7)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final p = await repo.getById(7);
|
||||||
|
expect(p?.id, 7);
|
||||||
|
expect(remote.getByIdCalls, 1);
|
||||||
|
expect(await local.getById(7), isNotNull); // mis en cache
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sans local (web), passe directement par l\'API', () async {
|
||||||
|
final remote = FakeRemote([_mk(9)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: null);
|
||||||
|
final p = await repo.getById(9);
|
||||||
|
expect(p?.id, 9);
|
||||||
|
expect(remote.getByIdCalls, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getAll', () {
|
||||||
|
test('si le cache est complet, ne rappelle pas l\'API', () async {
|
||||||
|
final store = {for (var i = 1; i <= 1025; i++) i: _mk(i)};
|
||||||
|
final local = FakeLocal(store);
|
||||||
|
final remote = FakeRemote([]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final list = await repo.getAll();
|
||||||
|
expect(list.length, 1025);
|
||||||
|
expect(remote.getAllCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('si le cache est incomplet, synchronise depuis l\'API', () async {
|
||||||
|
final local = FakeLocal({1: _mk(1)});
|
||||||
|
final remote = FakeRemote([_mk(1), _mk(2), _mk(3)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final list = await repo.getAll();
|
||||||
|
expect(remote.getAllCalls, 1);
|
||||||
|
expect(list.length, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('repli sur erreur', () {
|
||||||
|
test('getById retourne null si l\'API échoue et le cache est vide', () async {
|
||||||
|
final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: FakeLocal());
|
||||||
|
expect(await repo.getById(42), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAll retourne le cache existant si la synchro API échoue', () async {
|
||||||
|
final local = FakeLocal({1: _mk(1)}); // cache incomplet -> tente l'API, qui échoue
|
||||||
|
final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: local);
|
||||||
|
final list = await repo.getAll();
|
||||||
|
expect(list.length, 1); // repli sur le cache
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
124
test/domain/game_engine_test.dart
Normal file
124
test/domain/game_engine_test.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pokeguess/core/config/app_constants.dart';
|
||||||
|
import 'package:pokeguess/domain/entities/pokemon.dart';
|
||||||
|
import 'package:pokeguess/domain/game/game_engine.dart';
|
||||||
|
import 'package:pokeguess/domain/game/game_state.dart';
|
||||||
|
|
||||||
|
const _engine = GameEngine();
|
||||||
|
|
||||||
|
Pokemon _poke(String name) => Pokemon(
|
||||||
|
name: name, id: 1, type1: PokemonType.normal,
|
||||||
|
hp: 1, atk: 1, def: 1, spd: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
GameState _playing(String name, {bool shiny = false}) => _engine.startRound(
|
||||||
|
const GameState(), _poke(name), isShiny: shiny);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('newGame réinitialise vies/score/session, garde le bestScore', () {
|
||||||
|
const s = GameState(
|
||||||
|
lives: 1, currentScore: 50, sessionCorrectCount: 7, bestScore: 99);
|
||||||
|
final r = _engine.newGame(s);
|
||||||
|
expect(r.lives, 3);
|
||||||
|
expect(r.currentScore, 0);
|
||||||
|
expect(r.sessionCorrectCount, 0);
|
||||||
|
expect(r.bestScore, 99);
|
||||||
|
expect(r.status, GameStatus.loading);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startRound place le Pokémon et passe en playing', () {
|
||||||
|
final r = _playing('pikachu', shiny: true);
|
||||||
|
expect(r.currentPokemon?.name, 'pikachu');
|
||||||
|
expect(r.isShiny, true);
|
||||||
|
expect(r.isHintUsed, false);
|
||||||
|
expect(r.status, GameStatus.playing);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bonne réponse: +10 points et roundWon', () {
|
||||||
|
final o = _engine.submitGuess(_playing('pikachu'), 'pikachu');
|
||||||
|
expect(o.result, GuessResult.correct);
|
||||||
|
expect(o.state.currentScore, 10);
|
||||||
|
expect(o.state.sessionCorrectCount, 1);
|
||||||
|
expect(o.state.status, GameStatus.roundWon);
|
||||||
|
expect(o.state.currentPokemon?.isCaught, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bonne réponse shiny: +20 points', () {
|
||||||
|
final o = _engine.submitGuess(_playing('pikachu', shiny: true), 'pikachu');
|
||||||
|
expect(o.state.currentScore, 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('comparaison insensible aux accents et à la casse', () {
|
||||||
|
final o = _engine.submitGuess(_playing('Dracaufeu'), 'dracaufeu');
|
||||||
|
expect(o.result, GuessResult.correct);
|
||||||
|
final o2 = _engine.submitGuess(_playing('Électhor'), 'electhor');
|
||||||
|
expect(o2.result, GuessResult.correct);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mauvaise réponse: -1 vie, reste playing tant qu\'il reste des vies', () {
|
||||||
|
final o = _engine.submitGuess(_playing('pikachu'), 'salameche');
|
||||||
|
expect(o.result, GuessResult.wrong);
|
||||||
|
expect(o.state.lives, 2);
|
||||||
|
expect(o.state.status, GameStatus.playing);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mauvaise réponse à 1 vie: gameOver', () {
|
||||||
|
const start = GameState(lives: 1, status: GameStatus.playing);
|
||||||
|
final round = _engine.startRound(start, _poke('pikachu'), isShiny: false);
|
||||||
|
final o = _engine.submitGuess(round, 'faux');
|
||||||
|
expect(o.result, GuessResult.gameOver);
|
||||||
|
expect(o.state.lives, 0);
|
||||||
|
expect(o.state.status, GameStatus.gameOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le cheat "pikachu" n\'existe plus: faux nom sur un autre Pokémon = wrong', () {
|
||||||
|
final o = _engine.submitGuess(_playing('bulbizarre'), 'pikachu');
|
||||||
|
expect(o.result, GuessResult.wrong);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bonus: indice tous les 5, skip tous les 10', () {
|
||||||
|
var s = const GameState(sessionCorrectCount: 4, status: GameStatus.playing);
|
||||||
|
s = _engine.startRound(s, _poke('pikachu'), isShiny: false);
|
||||||
|
final o = _engine.submitGuess(s, 'pikachu'); // 5e bonne réponse
|
||||||
|
expect(o.state.sessionCorrectCount, 5);
|
||||||
|
expect(o.state.hints, appConstantsHints + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useHint consomme un indice', () {
|
||||||
|
final s = _playing('pikachu');
|
||||||
|
final r = _engine.useHint(s);
|
||||||
|
expect(r.isHintUsed, true);
|
||||||
|
expect(r.hints, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useHint sans indice restant ne change rien', () {
|
||||||
|
final s = _playing('pikachu').copyWith(hints: 0);
|
||||||
|
final r = _engine.useHint(s);
|
||||||
|
expect(r.isHintUsed, false);
|
||||||
|
expect(r.hints, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useSkip décrémente les skips', () {
|
||||||
|
final s = _playing('pikachu');
|
||||||
|
final r = _engine.useSkip(s);
|
||||||
|
expect(r.skips, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bonus: skip tous les 10 bonnes réponses', () {
|
||||||
|
var s = const GameState(sessionCorrectCount: 9, status: GameStatus.playing);
|
||||||
|
s = _engine.startRound(s, _poke('pikachu'), isShiny: false);
|
||||||
|
final o = _engine.submitGuess(s, 'pikachu'); // 10e bonne réponse
|
||||||
|
expect(o.state.sessionCorrectCount, 10);
|
||||||
|
expect(o.state.skips, AppConstants.startingSkips + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submitGuess renvoie invalid quand la manche n\'est pas en cours', () {
|
||||||
|
const s = GameState(status: GameStatus.loading); // aucun pokemon, pas en playing
|
||||||
|
final o = _engine.submitGuess(s, 'pikachu');
|
||||||
|
expect(o.result, GuessResult.invalid);
|
||||||
|
expect(identical(o.state, s), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valeur attendue de hints au démarrage (miroir d'AppConstants.startingHints = 3).
|
||||||
|
const appConstantsHints = 3;
|
||||||
@ -1,29 +1,11 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pokeguess/main.dart';
|
import 'package:pokeguess/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('L\'app démarre sans crash', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
await tester.pumpWidget(const ProviderScope(child: MyApp()));
|
||||||
await tester.pumpWidget(const MyApp());
|
expect(find.byType(MaterialApp), findsOneWidget);
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user