Compare commits

74 Commits
Author SHA1 Message Date
maxiwere45andClaude Opus 4.8 8608fc023c docs: sync ARCHITECTURE and README with actual codebase
Add missing providers (locale, theme, genFilter, caughtCount), all 6
pages, full widget inventory, core/l10n layers, and game mechanics
(hints, skips, shiny, bonuses, bilingual input, gen filter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:58:37 +02:00
adessen 8f93a18a70 patch guess langue issue 2026-06-23 14:39:16 +02:00
adessen 3d2f5be5e5 oups forget something 2026-06-23 14:26:59 +02:00
adessen 3984e94bc5 patch small issue with trad and theme 2026-06-23 14:17:27 +02:00
maxiwere45 47536843be hotfix: update README 2026-06-23 12:22:37 +02:00
maxiwere45 0952b372b4 hotfix: update default language 2026-06-23 12:16:38 +02:00
maxiwere45 9a41bd50a6 Merge: FR/EN internationalization (i18n) with language selector 2026-06-23 12:13:51 +02:00
maxiwere45andClaude Opus 4.8 a2a7ffd79f feat(i18n): add FR/EN internationalization with in-app language selector
- gen-l10n setup (flutter_localizations, intl, l10n.yaml, ARB en/fr)
- localeProvider (persisted) wired into MaterialApp
- language selector in the System page (alongside the color palette)
- all user-facing strings across screens moved to AppLocalizations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:13:51 +02:00
maxiwere45 409ab9c1dd Merge: update README for current project state 2026-06-23 12:00:50 +02:00
maxiwere45andClaude Opus 4.8 d13f8d8834 docs: rewrite README to reflect Pokéguess (game, architecture, stack)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:00:50 +02:00
maxiwere45 afacacaa10 Merge: document all public classes (code documentation) 2026-06-23 11:58:29 +02:00
maxiwere45andClaude Opus 4.8 7fd122fd91 docs(code): add doc comments to all public widgets and providers
Document MyApp, MainPage, PokemonImage/Tile/TypeWidget, and the gen-filter
and theme providers/notifiers. Every public class/enum now has a /// doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:58:29 +02:00
maxiwere45 9d49cef271 Merge: split pokemon_list into sub-widgets (<150 lines) 2026-06-23 11:53:27 +02:00
maxiwere45andClaude Opus 4.8 e93384bf71 refactor(list): split pokemon_list into sub-widgets (<150 lines)
Extract PokedexListHeader, PokedexCountBar; reuse shared ScanlineOverlay.
pokemon_list.dart 169 -> 128 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:53:27 +02:00
maxiwere45 b12739af09 Merge: split system_page into sub-widgets (<150 lines) 2026-06-23 11:51:15 +02:00
maxiwere45andClaude Opus 4.8 749314d312 refactor(system): extract system_page sub-widgets into separate files (<150 lines)
Move SystemHeader, SectionTitle, StatsGrid/StatItem and PalettePicker into
widgets/system/. system_page.dart 260 -> 80 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:51:15 +02:00
maxiwere45 dd33885bda Merge: split pokemon_detail into sub-widgets (<150 lines) 2026-06-23 11:49:19 +02:00
maxiwere45andClaude Opus 4.8 41a80f03f0 refactor(detail): split pokemon_detail into sub-widgets (<150 lines)
Extract PokemonDetailTop and PokemonStatsPanel (+ reusable _StatBar).
pokemon_detail.dart 286 -> 97 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:49:19 +02:00
maxiwere45 93e3fa76c8 Merge: split game_over_page into sub-widgets (<150 lines) 2026-06-23 11:47:08 +02:00
maxiwere45andClaude Opus 4.8 bdbd2fcc6a refactor(game-over): split game_over_page into sub-widgets (<150 lines)
Extract GameOverHeader, GameOverStats (+ reusable _StatBox), HingeDivider,
GameOverActions. game_over_page.dart 338 -> 100 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:47:08 +02:00
maxiwere45 2f0d7e1e36 Merge: split guess_page into sub-widgets (<150 lines) 2026-06-23 11:44:55 +02:00
maxiwere45andClaude Opus 4.8 cb9e214aa2 refactor(guess): split guess_page into focused sub-widgets (<150 lines)
Extract GuessSilhouette, GenFilterSection, LivesRow, GuessInputSection,
ScoreBoard and a shared ScanlineOverlay. guess_page.dart 440 -> 149 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:44:55 +02:00
maxiwere45 d12b01b4ab chore: add documents 2026-06-23 11:36:13 +02:00
maxiwere45 1b9618de84 Merge branch 'pokeguess-preview' 2026-06-23 11:34:23 +02:00
adessen 71f5122e74 setings and gen filter 2026-06-23 10:54:16 +02:00
maxiwere45andClaude Opus 4.8 d54e517ca6 chore(core): remove unused Result type (dead code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:33:51 +02:00
maxiwere45andClaude Opus 4.8 43b68d1352 docs: update architecture documentation for new layered design
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:24:33 +02:00
maxiwere45andClaude Opus 4.8 5c9cef99a7 test: fix default widget smoke test for new architecture
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:24:05 +02:00
maxiwere45andClaude Opus 4.8 ca77317a20 chore: remove legacy models/utils/api/database layers
Also moves stray lib/pages/quel-est-ce-pokemon.code-workspace to repo
root, and fixes two trivial test lint infos (curly_braces_in_flow_
control_structures, constant_identifier_names) to reach clean analyze.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:20:54 +02:00
maxiwere45andClaude Opus 4.8 dc7e681508 fix(presentation): recover from game-over back gesture, show final score, guard hint, constants
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:15:27 +02:00
maxiwere45andClaude Opus 4.8 e62c8a9034 refactor: wire main.dart to presentation layer
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:45:59 +02:00
maxiwere45andClaude Opus 4.8 2c21b80a03 refactor(presentation): guess page consumes gameProvider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:45:20 +02:00
maxiwere45andClaude Opus 4.8 4ac13bd233 refactor(presentation): game over page uses repository + constant
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:43:48 +02:00
maxiwere45andClaude Opus 4.8 7fd6d16020 refactor(presentation): pokemon list consumes pokedexProvider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:43:02 +02:00
maxiwere45andClaude Opus 4.8 d4c8936c80 refactor(presentation): main page as ConsumerWidget with nav provider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:42:13 +02:00
maxiwere45andClaude Opus 4.8 439c0101f4 refactor: move UI into presentation/widgets and presentation/pages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:41:41 +02:00
maxiwere45andClaude Opus 4.8 29d2c92b37 fix(presentation): resilient pokemon loading, web-safe invalidate, correct best-score persist
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:38:40 +02:00
maxiwere45andClaude Opus 4.8 4bca4b1ed5 feat(presentation): add game state Notifier
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:31:42 +02:00
maxiwere45andClaude Opus 4.8 2d87879af8 feat(presentation): add selected-tab navigation provider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:31:16 +02:00
maxiwere45andClaude Opus 4.8 d531fcb2c8 feat(presentation): add pokedex AsyncNotifier provider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:30:55 +02:00
maxiwere45andClaude Opus 4.8 f6a6ba2cd1 feat(presentation): add repository DI provider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:30:30 +02:00
maxiwere45andClaude Opus 4.8 e2e310cae5 test(domain): cover skip bonus and invalid-guess paths in GameEngine
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:29:03 +02:00
maxiwere45andClaude Opus 4.8 e3a9831ce1 feat(domain): add pure GameEngine with TDD (removes pikachu cheat)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:22:03 +02:00
maxiwere45andClaude Opus 4.8 0d977e5cca feat(domain): add immutable GameState
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:20:46 +02:00
maxiwere45 4bc3948fed feat(gitignore): add .claude/ to ignore list 2026-06-09 11:19:20 +02:00
maxiwere45andClaude Opus 4.8 6885771081 fix(data): race-safe DB open, graceful unknown types, explicit id error + fallback tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:18:56 +02:00
maxiwere45andClaude Opus 4.8 51c6ef904d feat(data): add PokemonRepository implementation + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:13:06 +02:00
maxiwere45andClaude Opus 4.8 42bd9f7c20 feat(domain): add PokemonRepository interface
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:12:05 +02:00
maxiwere45andClaude Opus 4.8 cbc742b25a feat(data): add HTTP remote datasource
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:11:31 +02:00
maxiwere45andClaude Opus 4.8 96758f1b6b feat(data): add instance-based SQLite local datasource
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:10:25 +02:00
maxiwere45andClaude Opus 4.8 f2dcba0fe2 feat(data): add PokemonDto with single-source JSON parsing + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:09:51 +02:00
maxiwere45andClaude Opus 4.8 2f051c8da6 refactor: point leaf components at domain Pokemon entity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:57:51 +02:00
maxiwere45andClaude Opus 4.8 b1f67d3daa feat(presentation): add type colors helper
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:57:23 +02:00
maxiwere45andClaude Opus 4.8 8ca6405bc0 feat(domain): add pure Pokemon entity and PokemonType enum
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:56:59 +02:00
maxiwere45andClaude Opus 4.8 8f29f3578a refactor(core): keep logger pure-Dart and document Result subtypes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:55:36 +02:00
maxiwere45andClaude Opus 4.8 2a3f489a2d feat(core): add AppLogger
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:50:31 +02:00
maxiwere45andClaude Opus 4.8 6b150945fa feat(core): add sealed Result type
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:50:16 +02:00
maxiwere45andClaude Opus 4.8 03dbdd723d feat(core): add centralized app constants
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:49:51 +02:00
maxiwere45andClaude Opus 4.8 ca769dca2d chore: add flutter_riverpod and wrap app in ProviderScope
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:48:53 +02:00
maxiwere45andClaude Opus 4.8 6f81384a06 chore: gitignore local superpowers docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:39:29 +02:00
maxiwere45 4faf259aaa feat: Introduce PokemonImage component for robust image loading with fallback and shiny support, and update iOS platform to 15.0. 2026-03-20 11:23:22 +01:00
adessen 8cca5a64de Merge branch 'pokeguess-preview' of https://gitea.losvernos.com/guillaumev/quel-est-ce-pokemon into pokeguess-preview 2026-03-20 10:44:42 +01:00
adessen d3d3ba3586 app perfect 2026-03-20 10:42:30 +01:00
maxiwere45 6592b35755 docs: Add initial project README and architecture documentation. 2026-03-17 16:05:17 +01:00
maxiwere45 112d0136c9 feat: Implement batch insertion for Pokémon and utilize it for more efficient initial data synchronization. 2026-03-17 15:09:41 +01:00
maxiwere45 fbf37e6861 chore: major changes 2026-03-17 14:57:39 +01:00
maxiwere45 528cdcafef init v2 app 2026-03-17 13:26:21 +01:00
maxiwere45 5f75c53866 fix: test broken 2026-02-03 14:38:20 +01:00
maxiwere45 e18863cf1c feat: add frenchTypeToEnum function to map French Pokémon types to enums 2026-02-03 09:47:21 +01:00
maxiwere45 5bd3b1f18f fix: update image URLs to use TyraDex for Pokémon sprites 2026-02-03 09:47:15 +01:00
maxiwere45 5b1ffaede8 fix: update Pokemon API integration to use Tyradex and improve type handling 2026-02-03 09:46:51 +01:00
maxiwere45 c7be5241a2 fix: remove unused sqflite_common import in main.dart 2026-02-03 09:46:41 +01:00
maxiwere45 f11e3f7b63 chore: update Podfile.lock to include sqflite_darwin and sqlite3_flutter_libs dependencies 2026-02-03 09:46:34 +01:00
guillaumev 652eb9072f fix: android build 2026-02-02 15:01:02 +01:00
106 changed files with 5794 additions and 674 deletions
+5
View File
@@ -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/
+5 -20
View File
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31" revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
channel: "stable" channel: "stable"
project_type: app project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: android - platform: android
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
- platform: linux
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
- platform: macos
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
- platform: web
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
- platform: windows
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
# User provided section # User provided section
+70 -25
View File
@@ -1,37 +1,82 @@
# pokedex # Pokéguess
Projet d'exemple d'application Flutter pour les cours de bootcamp Ynov 2023. > Devine le Pokémon à partir de sa silhouette !
Ce projet contient un début de pokédex, une application listant des pokémons et permettant d'en voir les détails.
Application mobile Flutter développée dans le cadre du cours **DEV MOBILE** (Ynov).
À l'origine basée sur l'exemple de pokédex du bootcamp, le projet est devenu un jeu
complet « Who's that Pokémon ? » adossé à un pokédex.
## Fonctionnalités
- **Jeu de devinette** : une silhouette de Pokémon à identifier, avec un système de
vies, d'indices et de passes (skips), des Pokémon **shiny** (bonus de points), un
score courant et un meilleur score persistant.
- **Filtre par génération** : on choisit les générations (I à IX) dont les Pokémon
peuvent tomber pendant le jeu.
- **Pokédex** : liste nationale des Pokémon avec filtre `ALL` / `CAUGHT` et compteur de
progression (découverts / total).
- **Fiche détaillée** : statistiques de base, description, types, bascule normal/shiny.
- **Page Système** : statistiques globales (attrapés, vus, complétion, meilleur score)
et choix de la **palette de couleurs** de l'app.
- **Offline-first** : les données sont mises en cache localement (SQLite) ; l'app reste
utilisable hors-ligne une fois la synchronisation initiale faite.
## Architecture
Le projet suit une **Clean Architecture allégée** en trois couches, avec **Riverpod**
pour la gestion d'état :
```text
lib/
├── core/ # constantes, logger (Dart pur)
├── domain/ # entités, interface de repository, moteur de jeu (Dart pur, testé)
├── data/ # DTO de parsing, datasources SQLite/HTTP, implémentation du repository
└── presentation/ # providers Riverpod, pages et widgets (chacun < 150 lignes)
```
La règle de dépendance pointe vers l'intérieur : `domain` ne dépend de rien,
`data` et `presentation` dépendent de `domain`. Détails dans
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
## Stack technique
- [Flutter](https://flutter.dev) / Dart
- [flutter_riverpod](https://pub.dev/packages/flutter_riverpod) — gestion d'état
- [sqflite](https://pub.dev/packages/sqflite) — persistance locale (SQLite)
- [http](https://pub.dev/packages/http) — appels réseau
- [shared_preferences](https://pub.dev/packages/shared_preferences) — préférences (meilleur score, palette)
- [google_fonts](https://pub.dev/packages/google_fonts) — police rétro
## Installation ## Installation
```sh ```sh
git clone https://github.com/Efilown/pokedex-example.git git clone https://gitea.losvernos.com/guillaumev/quel-est-ce-pokemon.git
cd pokedex-example cd quel-est-ce-pokemon
flutter pub get flutter pub get
``` ```
Ensuite, ouvrir le projet avec un IDE (tel que vscode) et le lancer sur le device de votre choix (téléphone, émulateur ou navigateur). Puis lancer l'application sur le device de votre choix (téléphone, émulateur ou
navigateur) :
```sh
flutter run
```
## Tests
```sh
flutter test
```
Les tests couvrent le moteur de jeu (`GameEngine`), le parsing (`PokemonDto`) et le
repository.
## Ressources utilisées ## Ressources utilisées
Pour les images (remplacer par l'id du pokémon désiré): - [API Tyradex](https://tyradex.app) — données des Pokémon (français)
- Sprites officiels via le dépôt [TyraDex/sprites](https://raw.githubusercontent.com/Yarkis01/TyraDex/images/sprites/)
(remplacer `<id>` par le numéro du Pokémon) :
- normal : `.../sprites/<id>/regular.png`
- shiny : `.../sprites/<id>/shiny.png`
- [Cris des Pokémon](https://pokemoncries.com/cries/1.mp3)
- [Sprite normal](https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png)
- [Sprite shiny](https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/6.png)
Autres liens
- [l'API pour récupérer les informations](https://pokeapi.co/api/v2/pokemon/6)
- [Les cris de bataille](https://pokemoncries.com/cries/1.mp3)
- [La documentation du paquet SQFlite](https://pub.dev/packages/sqflite)
- [La documentation du paquet HTTP](https://pub.dev/packages/http)
## Améliorations possible
- Mettre un lecteur audio pour écouter les cris des pokémons
- Rajouter la traduction (en fonction de la langue de l'application ? ☺)
- Compléter la vue détail avec des informations tirées de l'API
- Faire une vue "mon équipe" contenant 6 pokémons choisis en rajoutant un bouton "ajouter à mon équipe" sur la vue détail.
Amusez-vous !
+2 -1
View File
@@ -5,9 +5,10 @@ gradle-wrapper.jar
/gradlew.bat /gradlew.bat
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app # See https://flutter.dev/to/reference-keystore
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
-67
View File
@@ -1,67 +0,0 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.pokedex"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.pokedex"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {}
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.pokeguess"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.pokeguess"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
+14
View File
@@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application <application
android:label="pokeguess" android:label="pokeguess"
android:name="${applicationName}" android:name="${applicationName}"
@@ -7,6 +9,7 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
@@ -30,4 +33,15 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest> </manifest>
@@ -1,6 +0,0 @@
package com.example.pokedex
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
@@ -0,0 +1,5 @@
package com.example.pokeguess
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
-31
View File
@@ -1,31 +0,0 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -1,3 +1,2 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true
+3 -1
View File
@@ -2,4 +2,6 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
org.gradle.caching=true
org.gradle.parallel=true
-20
View File
@@ -1,20 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
include ":app"
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+3
View 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:
+105
View File
@@ -0,0 +1,105 @@
# 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
### core (transversal)
- **`AppConstants`** : toutes les constantes métier en un seul endroit (vies, points, shiny odds,
plages de générations, clés SharedPreferences, URL API).
- **`logger`** : wrapper minimal autour de `dart:developer`.
### 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).
- `GameEngine` gère : soumission de réponse, vies, hints, skips, score, Shiny, bonus périodiques.
- `GameStatus` : `loading | playing | roundWon | gameOver`.
- `GuessResult` : `correct | wrong | gameOver | invalid`.
### data
- **DTO** : `PokemonDto` centralise tout le parsing JSON (API Tyradex + SQLite).
- `fromTyradexJson()`, `fromDb()`, `toDb()`.
- **Datasources** :
- `PokemonLocalDataSource` — SQLite via sqflite ; absent (`null`) sur le web.
- `PokemonRemoteDataSource` — HTTP vers Tyradex + PokéAPI (genus anglais).
- **Repository (impl)** : `PokemonRepositoryImpl` applique « DB locale d'abord, sinon API + cache ».
### l10n
Internationalisation complète **FR / EN** via `flutter_localizations` + `intl`.
Fichiers générés : `app_localizations.dart`, `app_localizations_fr.dart`, `app_localizations_en.dart`.
La langue active est persistée dans SharedPreferences (`AppConstants.prefsLocale`).
### presentation
#### Providers (Riverpod)
| Provider | Type | Rôle |
| --- | --- | --- |
| `pokemonRepositoryProvider` | `Provider` | DI du repository |
| `pokedexProvider` | `AsyncNotifier<List<Pokemon>>` | Chargement + cache du Pokédex |
| `gameProvider` | `Notifier<GameState>` | État de la partie en cours |
| `selectedTabProvider` | `StateProvider<int>` | Onglet de navigation courant |
| `localeProvider` | `Notifier<Locale>` | Langue active (FR/EN) + persistance |
| `themeProvider` | `Notifier<AppPalette>` | Palette de couleurs active (5 thèmes) + persistance |
| `genFilterProvider` | `Notifier<Set<int>>` | Générations sélectionnées pour le jeu + persistance |
| `caughtCountProvider` | `Provider<int>` | Nombre de Pokémon capturés (dérivé de pokedexProvider) |
#### Pages
| Fichier | Rôle |
| --- | --- |
| `main_page.dart` | Hub de navigation par onglets (Jeu / Pokédex / Système) |
| `guess_page.dart` | Écran principal du jeu (silhouette, input, lives, score) |
| `game_over_page.dart` | Écran de fin de partie avec stats de session |
| `pokemon_list.dart` | Pokédex : liste filtrée + recherche par nom |
| `pokemon_detail.dart` | Fiche détaillée : stats, type, genus, toggle normal/shiny |
| `system_page.dart` | Paramètres (langue, palette) et statistiques globales |
#### Widgets notables
- **Réutilisables** : `PokemonImage`, `PokemonTile`, `PokemonTypeWidget`, `ScanlineOverlay`.
- **guess/** : `GuessSilhouette`, `GuessInputSection`, `LivesRow`, `ScoreBoard`, `GenFilterSection`.
- **detail/** : `PokemonDetailTop`, `PokemonStatsPanel`.
- **game_over/** : `GameOverHeader`, `GameOverStats`, `GameOverActions`, `HingeDivider`.
- **list/** : `PokedexListHeader`, `PokedexCountBar`.
- **system/** : `LanguagePicker`, `PalettePicker`, `SectionTitle`, `SystemHeader`, `SystemStats`.
#### Thème
`type_colors.dart` : couleur et libellé localisé par type Pokémon.
## 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.
## Mécaniques de jeu (résumé)
- **Vies** : 3 au départ ; une vie perdue par mauvaise réponse ; game over à 0.
- **Hints** : 3 au départ ; révèle partiellement le nom ; +1 tous les 5 bonnes réponses consécutives.
- **Skips** : 3 au départ ; passe au Pokémon suivant sans pénalité ; +1 tous les 10 bonnes réponses.
- **Score** : +10 pts normal, +20 pts shiny ; meilleur score persisté via SharedPreferences.
- **Shiny** : 1 chance sur 10 (`AppConstants.shinyOdds`).
- **Filtre de génération** : le joueur choisit parmi Gen IIX ; persisté entre les sessions.
- **Acceptation bilingue** : le nom FR *ou* EN est accepté quelle que soit la langue de l'UI.
## Tests
- `test/domain/game_engine_test.dart` : règles du jeu.
- `test/data/pokemon_dto_test.dart` : parsing JSON ↔ DB.
- `test/data/pokemon_repository_test.dart` : logique du repository (datasources factices).
- `test/widget_test.dart` : smoke test de démarrage de l'app.
Binary file not shown.
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
# Pokeguess
## Description
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokémon through
a silhouette guessing game. The app fetches data from the Tyradex API and stores it locally for
offline access.
## Features
- **National Pokédex**: Browse all 1025 Pokémon from generations I to IX.
- **Guess Game**: Identify Pokémon by their silhouette.
- **Scoring System**: Earn 10 pts per correct guess, 20 pts for Shiny Pokémon (1 in 10 chance).
High scores are saved locally.
- **Lives, Hints & Skips**: Start each game with 3 lives, 3 hints, and 3 skips. Earn bonuses
every 5 correct guesses (hint) and every 10 correct guesses (skip).
- **Generation Filter**: Restrict the game to one or more generations (IIX). Selection persists
across sessions.
- **Bilingual Input**: Pokémon names are accepted in French or English regardless of the UI language.
- **Collection**: Track caught and seen Pokémon across your Pokédex.
- **Search and Filter**: Filter the collection by all / caught status and search by name.
- **Pokémon Detail**: View base stats, types, genus, and toggle between normal and shiny sprites.
- **Theming**: Choose from 5 color palettes (red, blue, green, yellow, purple).
- **Localization**: Full French and English UI support, switchable at any time.
- **Statistics**: Global view of caught count, seen count, completion percentage, and best score.
## 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.
- **Riverpod**: State management (providers + notifiers).
- **SQLite (sqflite)**: Local database for offline-first Pokémon storage.
- **Tyradex API**: Primary Pokémon data source (FR/EN names, sprites, types, stats).
- **PokéAPI**: Secondary source for English genus data.
- **Shared Preferences**: Persistence for best score, generation filter, language, and palette.
- **Google Fonts**: Custom typography (VT323).
- **flutter_localizations / intl**: FR and EN localization.
Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

-2
View File
@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict> </dict>
</plist> </plist>
+4 -1
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '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
+46 -15
View File
@@ -1,33 +1,64 @@
PODS: PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- FMDB (2.7.12): - shared_preferences_foundation (0.0.1):
- FMDB/standard (= 2.7.12)
- FMDB/Core (2.7.12)
- FMDB/standard (2.7.12):
- FMDB/Core
- sqflite (0.0.3):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.52.0):
- sqlite3/common (= 3.52.0)
- sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.52.0):
- sqlite3/common
- sqlite3/fts5 (3.52.0):
- sqlite3/common
- sqlite3/math (3.52.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common
- sqlite3/rtree (3.52.0):
- sqlite3/common
- sqlite3/session (3.52.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - sqlite3
EXTERNAL SOURCES: EXTERNAL SOURCES:
Flutter: Flutter:
:path: Flutter :path: Flutter
sqflite: shared_preferences_foundation:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite: 5b24d06a453c198c13b305ceea4b4286cc07cfe4 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 PODFILE CHECKSUM: 4b015915ec662986b54bf30ab778da63f7dda016
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+3 -3
View File
@@ -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"
+6 -3
View File
@@ -1,13 +1,16 @@
import UIKit
import Flutter import Flutter
import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }
+25 -4
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -24,6 +26,29 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -41,9 +66,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>
+4
View File
@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n
-34
View File
@@ -1,34 +0,0 @@
import '../models/pokemon.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
// 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 = 'pokeapi.co';
static const String pokemonUrl = 'api/v2/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
// URI.https prends en paramètre le nom de domaine et le chemin 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);
String name = json['name'];
String type1 = (json['types'].length > 0 && json['types'][0]['type'] != null && json['types'][0]['type']['name'] != null) ? json['types'][0]['type']['name'] : "unknown";
String? type2 = (json['types'].length > 1 && json['types'][1]['type'] != null && json['types'][1]['type']['name'] != null) ? json['types'][1]['type']['name'] : null;
// On crée un objet Pokemon à partir du fichier JSON
return Pokemon(
name: name,
id: id,
type1: PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type1'),
type2: type2 != null ? PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type2') : null,
);
}
}
-52
View File
@@ -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
),
),
],
)
),
),
);
}
}
+59
View File
@@ -0,0 +1,59 @@
/// 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';
/// Clé SharedPreferences pour le filtre de générations.
static const String prefsGenFilter = 'gen_filter';
/// Clé SharedPreferences pour la langue de l'application.
static const String prefsLocale = 'locale_code';
/// Plages d'IDs Pokémon par génération [min, max] (inclusif).
static const List<(int, int)> genRanges = [
(1, 151), // Gen 1
(152, 251), // Gen 2
(252, 386), // Gen 3
(387, 493), // Gen 4
(494, 649), // Gen 5
(650, 721), // Gen 6
(722, 809), // Gen 7
(810, 905), // Gen 8
(906, 1025), // Gen 9
];
}
+21
View 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;
}());
}
}
@@ -0,0 +1,88 @@
import 'package:sqflite_common/sqflite.dart';
import '../../domain/entities/pokemon.dart';
import '../dto/pokemon_dto.dart';
const _createSql = '''
CREATE TABLE IF NOT EXISTS pokemon (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
name_fr TEXT,
name_en TEXT,
type1 TEXT NOT NULL,
type2 TEXT,
hp INTEGER NOT NULL,
atk INTEGER NOT NULL,
def INTEGER NOT NULL,
spd INTEGER NOT NULL,
description TEXT,
description_en TEXT,
isCaught INTEGER NOT NULL DEFAULT 0,
isSeen INTEGER NOT NULL DEFAULT 0
)
''';
class PokemonLocalDataSource {
final String languageCode;
PokemonLocalDataSource({this.languageCode = 'fr'});
Future<Database>? _dbFuture;
Future<Database> _getDb() {
return _dbFuture ??= openDatabase(
'pokedex.db',
version: 5,
onUpgrade: (db, oldVersion, newVersion) async {
// Drop and recreate on any upgrade to pick up new columns cleanly.
await db.execute('DROP TABLE IF EXISTS pokemon');
await db.execute(_createSql);
},
onCreate: (db, version) async {
await db.execute(_createSql);
},
);
}
Future<List<Pokemon>> getAll() async {
final db = await _getDb();
final rows = await db.query('pokemon');
return rows.map((r) => PokemonDto.fromDb(r, languageCode: languageCode)).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, languageCode: languageCode);
}
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;
}
}
@@ -0,0 +1,68 @@
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;
final String languageCode;
PokemonRemoteDataSource({http.Client? client, this.languageCode = 'fr'})
: _client = client ?? http.Client();
Future<Pokemon> getById(int id) async {
AppLogger.info('API: fetching Pokémon $id (lang: $languageCode)');
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, languageCode: languageCode);
}
/// Fetches the English genus (category) from PokéAPI for a given Pokémon id.
/// Returns null on any error (network, missing entry, etc.).
Future<String?> getEnglishGenus(int id) async {
try {
final response = await _client
.get(Uri.https('pokeapi.co', '/api/v2/pokemon-species/$id/'));
if (response.statusCode != 200) return null;
final json = jsonDecode(response.body) as Map<String, dynamic>;
final genera = json['genera'] as List<dynamic>? ?? [];
for (final g in genera) {
final lang = (g['language'] as Map<String, dynamic>?)?['name'];
if (lang == 'en') return g['genus'] as String?;
}
} catch (_) {}
return null;
}
Future<List<Pokemon>> getAll() async {
AppLogger.info('API: fetching ALL Pokémon (lang: $languageCode)');
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;
try {
result.add(PokemonDto.fromTyradexJson(
json as Map<String, dynamic>,
languageCode: languageCode,
));
} catch (e, st) {
AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st);
}
}
return result;
}
}
+133
View File
@@ -0,0 +1,133 @@
import '../../domain/entities/pokemon.dart';
/// Conversion JSON <-> entité Pokemon. Unique endroit de parsing.
class PokemonDto {
PokemonDto._();
static PokemonType _frenchTypeToEnum(String type) {
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[type] ?? PokemonType.unknown;
}
// Tyradex always returns type names in French regardless of endpoint language.
static PokemonType _typeToEnum(String typeName, String languageCode) {
return _frenchTypeToEnum(typeName);
}
/// Construit une entité depuis la réponse Tyradex.
/// [languageCode] détermine quelle langue utiliser pour [name] ('fr' ou 'en').
static Pokemon fromTyradexJson(
Map<String, dynamic> json, {
int? fallbackId,
String languageCode = 'fr',
}) {
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 nameFr = nameMap?['fr'] as String? ?? 'unknown';
final nameEn = nameMap?['en'] as String? ?? nameFr;
final name = languageCode == 'en' ? nameEn : nameFr;
final List types = json['types'] ?? [];
// Tyradex retourne toujours les types dans la langue du endpoint (fr ou en).
final type1 = types.isNotEmpty
? _typeToEnum(types[0]['name'] as String, languageCode)
: PokemonType.unknown;
final type2 = types.length > 1
? _typeToEnum(types[1]['name'] as String, languageCode)
: null;
final Map<String, dynamic>? stats = json['stats'];
// Tyradex expose 'category' (FR) et parfois 'category_en' (EN).
final descFr = json['category'] as String?;
final descEn = (json['category_en'] ?? json['category']) as String?;
return Pokemon(
name: name,
nameFr: nameFr,
nameEn: nameEn,
id: id,
type1: type1,
type2: type2,
hp: stats?['hp'] ?? 0,
atk: stats?['atk'] ?? 0,
def: stats?['def'] ?? 0,
spd: stats?['vit'] ?? 0,
description: descFr,
descriptionEn: descEn,
);
}
/// Sérialise pour SQLite (stocke les deux noms et descriptions).
static Map<String, dynamic> toDb(Pokemon p) {
return {
'name': p.name,
'name_fr': p.nameFr ?? p.name,
'name_en': p.nameEn ?? 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,
'description_en': p.descriptionEn,
'isCaught': p.isCaught ? 1 : 0,
'isSeen': p.isSeen ? 1 : 0,
};
}
/// Reconstruit depuis une ligne SQLite.
/// [languageCode] détermine quel nom afficher.
static Pokemon fromDb(Map<String, dynamic> row, {String languageCode = 'fr'}) {
final nameFr = row['name_fr'] as String? ?? row['name'] as String;
final nameEn = row['name_en'] as String? ?? nameFr;
final name = languageCode == 'en' ? nameEn : nameFr;
return Pokemon(
name: name,
nameFr: nameFr,
nameEn: nameEn,
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'],
descriptionEn: row['description_en'] as String?,
isCaught: row['isCaught'] == 1 || row['isCaught'] == true,
isSeen: row['isSeen'] == 1 || row['isSeen'] == true,
);
}
}
@@ -0,0 +1,66 @@
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;
@override
Future<String?> getEnglishGenus(int id) => remote.getEnglishGenus(id);
}
-66
View File
@@ -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);
}
}
+83
View File
@@ -0,0 +1,83 @@
/// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API.
class Pokemon {
final String name;
final String? nameFr;
final String? nameEn;
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 String? descriptionEn;
final bool isCaught;
final bool isSeen;
const Pokemon({
required this.name,
this.nameFr,
this.nameEn,
required this.id,
required this.type1,
this.type2,
required this.hp,
required this.atk,
required this.def,
required this.spd,
this.description,
this.descriptionEn,
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,
String? nameFr,
String? nameEn,
int? id,
PokemonType? type1,
PokemonType? type2,
int? hp,
int? atk,
int? def,
int? spd,
String? description,
String? descriptionEn,
bool? isCaught,
bool? isSeen,
}) {
return Pokemon(
name: name ?? this.name,
nameFr: nameFr ?? this.nameFr,
nameEn: nameEn ?? this.nameEn,
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,
descriptionEn: descriptionEn ?? this.descriptionEn,
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
}
+103
View File
@@ -0,0 +1,103 @@
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());
// Accept the name in either language so a language switch mid-game never blocks the player.
final acceptedNames = <String>{
pokemon.name,
if (pokemon.nameFr != null) pokemon.nameFr!,
if (pokemon.nameEn != null) pokemon.nameEn!,
}.map((n) => _normalize(n.toLowerCase())).toSet();
if (acceptedNames.contains(normalizedGuess)) {
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
View 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);
}
@@ -0,0 +1,25 @@
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();
/// Catégorie anglaise d'un Pokémon (ex. "Seed Pokémon") depuis PokéAPI. Null si indisponible.
Future<String?> getEnglishGenus(int id);
}
+70
View File
@@ -0,0 +1,70 @@
{
"@@locale": "en",
"appTitle": "Pokéguess",
"whosThatPokemon": "WHO'S THAT POKÉMON?",
"shinyDetected": "✨ SHINY POKÉMON DETECTED! ✨",
"genFilter": "GEN FILTER",
"identificationInput": "IDENTIFICATION INPUT",
"hintPrefix": "HINT",
"enterPokemonName": "Enter Pokémon name...",
"continueButton": "CONTINUE",
"guessButton": "GUESS!",
"hintButton": "HINT ({count})",
"@hintButton": { "placeholders": { "count": { "type": "int" } } },
"skipButton": "SKIP ({count})",
"@skipButton": { "placeholders": { "count": { "type": "int" } } },
"currentScore": "CURRENT SCORE",
"personalBest": "PERSONAL BEST: {score}",
"@personalBest": { "placeholders": { "score": { "type": "int" } } },
"caughtShiny": "✨ SHINY! You caught {name}! (+20 pts) ✨",
"@caughtShiny": { "placeholders": { "name": { "type": "String" } } },
"caughtNormal": "Correct! You caught {name}!",
"@caughtNormal": { "placeholders": { "name": { "type": "String" } } },
"wrongGuess": "Wrong guess! Try again.",
"errorLoadingPokemon": "Error loading Pokémon",
"listNational": "LIST - NATIONAL",
"tabAll": "ALL",
"tabCaught": "CAUGHT",
"pokemonDiscovered": "POKEMON DISCOVERED",
"noPokemonFound": "NO POKEMON FOUND IN {filter}",
"@noPokemonFound": { "placeholders": { "filter": { "type": "String" } } },
"pokedexFooter": "NATIONAL POKEDEX V2.0",
"loadingError": "Loading error",
"baseStats": "BASE STATS",
"noDescription": "No description available for this Pokémon.",
"gameOver": "GAME OVER",
"itWas": "It was {name}!",
"@itWas": { "placeholders": { "name": { "type": "String" } } },
"gameOverMessage": "\"Looks like your journey ends here. You've run out of energy!\"",
"statStreak": "STREAK",
"statSeen": "SEEN",
"statScore": "SCORE",
"tryAgain": "TRY AGAIN",
"backToPokedex": "BACK TO POKEDEX",
"system": "SYSTEM",
"statistics": "STATISTICS",
"colorPalette": "COLOR PALETTE",
"language": "LANGUAGE",
"statBestScore": "Best score",
"statCaught": "Caught",
"statSeenLabel": "Seen",
"statCompletion": "Completion",
"navList": "LIST",
"navGuess": "GUESS",
"navSystem": "SYSTEM",
"langEnglish": "English",
"langFrench": "French",
"palette0": "Pokédex Red",
"palette1": "Blue Ocean",
"palette2": "Green Forest",
"palette3": "Lightning Yellow",
"palette4": "Purple Shadow"
}
+63
View File
@@ -0,0 +1,63 @@
{
"@@locale": "fr",
"appTitle": "Pokéguess",
"whosThatPokemon": "QUI EST CE POKÉMON ?",
"shinyDetected": "✨ POKÉMON SHINY DÉTECTÉ ! ✨",
"genFilter": "FILTRE GÉN.",
"identificationInput": "SAISIE D'IDENTIFICATION",
"hintPrefix": "INDICE",
"enterPokemonName": "Entrez le nom du Pokémon...",
"continueButton": "CONTINUER",
"guessButton": "DEVINER !",
"hintButton": "INDICE ({count})",
"skipButton": "PASSER ({count})",
"currentScore": "SCORE ACTUEL",
"personalBest": "MEILLEUR SCORE : {score}",
"caughtShiny": "✨ SHINY ! Tu as attrapé {name} ! (+20 pts) ✨",
"caughtNormal": "Correct ! Tu as attrapé {name} !",
"wrongGuess": "Mauvaise réponse ! Réessaie.",
"errorLoadingPokemon": "Erreur de chargement du Pokémon",
"listNational": "LISTE - NATIONALE",
"tabAll": "TOUS",
"tabCaught": "ATTRAPÉS",
"pokemonDiscovered": "POKÉMON DÉCOUVERTS",
"noPokemonFound": "AUCUN POKÉMON DANS {filter}",
"pokedexFooter": "POKÉDEX NATIONAL V2.0",
"loadingError": "Erreur de chargement",
"baseStats": "STATS DE BASE",
"noDescription": "Aucune description disponible pour ce Pokémon.",
"gameOver": "PARTIE TERMINÉE",
"itWas": "C'était {name} !",
"gameOverMessage": "« On dirait que ton aventure s'arrête ici. Tu n'as plus d'énergie ! »",
"statStreak": "SÉRIE",
"statSeen": "VUS",
"statScore": "SCORE",
"tryAgain": "REJOUER",
"backToPokedex": "RETOUR AU POKÉDEX",
"system": "SYSTÈME",
"statistics": "STATISTIQUES",
"colorPalette": "PALETTE DE COULEURS",
"language": "LANGUE",
"statBestScore": "Meilleur score",
"statCaught": "Attrapés",
"statSeenLabel": "Vus",
"statCompletion": "Complétion",
"navList": "LISTE",
"navGuess": "DEVINER",
"navSystem": "SYSTÈME",
"langEnglish": "Anglais",
"langFrench": "Français",
"palette0": "Pokédex Rouge",
"palette1": "Océan Bleu",
"palette2": "Forêt Verte",
"palette3": "Foudre Jaune",
"palette4": "Ombre Violette"
}
+445
View File
@@ -0,0 +1,445 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_fr.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('fr')
];
/// No description provided for @appTitle.
///
/// In en, this message translates to:
/// **'Pokéguess'**
String get appTitle;
/// No description provided for @whosThatPokemon.
///
/// In en, this message translates to:
/// **'WHO\'S THAT POKÉMON?'**
String get whosThatPokemon;
/// No description provided for @shinyDetected.
///
/// In en, this message translates to:
/// **'✨ SHINY POKÉMON DETECTED! ✨'**
String get shinyDetected;
/// No description provided for @genFilter.
///
/// In en, this message translates to:
/// **'GEN FILTER'**
String get genFilter;
/// No description provided for @identificationInput.
///
/// In en, this message translates to:
/// **'IDENTIFICATION INPUT'**
String get identificationInput;
/// No description provided for @hintPrefix.
///
/// In en, this message translates to:
/// **'HINT'**
String get hintPrefix;
/// No description provided for @enterPokemonName.
///
/// In en, this message translates to:
/// **'Enter Pokémon name...'**
String get enterPokemonName;
/// No description provided for @continueButton.
///
/// In en, this message translates to:
/// **'CONTINUE'**
String get continueButton;
/// No description provided for @guessButton.
///
/// In en, this message translates to:
/// **'GUESS!'**
String get guessButton;
/// No description provided for @hintButton.
///
/// In en, this message translates to:
/// **'HINT ({count})'**
String hintButton(int count);
/// No description provided for @skipButton.
///
/// In en, this message translates to:
/// **'SKIP ({count})'**
String skipButton(int count);
/// No description provided for @currentScore.
///
/// In en, this message translates to:
/// **'CURRENT SCORE'**
String get currentScore;
/// No description provided for @personalBest.
///
/// In en, this message translates to:
/// **'PERSONAL BEST: {score}'**
String personalBest(int score);
/// No description provided for @caughtShiny.
///
/// In en, this message translates to:
/// **'✨ SHINY! You caught {name}! (+20 pts) ✨'**
String caughtShiny(String name);
/// No description provided for @caughtNormal.
///
/// In en, this message translates to:
/// **'Correct! You caught {name}!'**
String caughtNormal(String name);
/// No description provided for @wrongGuess.
///
/// In en, this message translates to:
/// **'Wrong guess! Try again.'**
String get wrongGuess;
/// No description provided for @errorLoadingPokemon.
///
/// In en, this message translates to:
/// **'Error loading Pokémon'**
String get errorLoadingPokemon;
/// No description provided for @listNational.
///
/// In en, this message translates to:
/// **'LIST - NATIONAL'**
String get listNational;
/// No description provided for @tabAll.
///
/// In en, this message translates to:
/// **'ALL'**
String get tabAll;
/// No description provided for @tabCaught.
///
/// In en, this message translates to:
/// **'CAUGHT'**
String get tabCaught;
/// No description provided for @pokemonDiscovered.
///
/// In en, this message translates to:
/// **'POKEMON DISCOVERED'**
String get pokemonDiscovered;
/// No description provided for @noPokemonFound.
///
/// In en, this message translates to:
/// **'NO POKEMON FOUND IN {filter}'**
String noPokemonFound(String filter);
/// No description provided for @pokedexFooter.
///
/// In en, this message translates to:
/// **'NATIONAL POKEDEX V2.0'**
String get pokedexFooter;
/// No description provided for @loadingError.
///
/// In en, this message translates to:
/// **'Loading error'**
String get loadingError;
/// No description provided for @baseStats.
///
/// In en, this message translates to:
/// **'BASE STATS'**
String get baseStats;
/// No description provided for @noDescription.
///
/// In en, this message translates to:
/// **'No description available for this Pokémon.'**
String get noDescription;
/// No description provided for @gameOver.
///
/// In en, this message translates to:
/// **'GAME OVER'**
String get gameOver;
/// No description provided for @itWas.
///
/// In en, this message translates to:
/// **'It was {name}!'**
String itWas(String name);
/// No description provided for @gameOverMessage.
///
/// In en, this message translates to:
/// **'\"Looks like your journey ends here. You\'ve run out of energy!\"'**
String get gameOverMessage;
/// No description provided for @statStreak.
///
/// In en, this message translates to:
/// **'STREAK'**
String get statStreak;
/// No description provided for @statSeen.
///
/// In en, this message translates to:
/// **'SEEN'**
String get statSeen;
/// No description provided for @statScore.
///
/// In en, this message translates to:
/// **'SCORE'**
String get statScore;
/// No description provided for @tryAgain.
///
/// In en, this message translates to:
/// **'TRY AGAIN'**
String get tryAgain;
/// No description provided for @backToPokedex.
///
/// In en, this message translates to:
/// **'BACK TO POKEDEX'**
String get backToPokedex;
/// No description provided for @system.
///
/// In en, this message translates to:
/// **'SYSTEM'**
String get system;
/// No description provided for @statistics.
///
/// In en, this message translates to:
/// **'STATISTICS'**
String get statistics;
/// No description provided for @colorPalette.
///
/// In en, this message translates to:
/// **'COLOR PALETTE'**
String get colorPalette;
/// No description provided for @language.
///
/// In en, this message translates to:
/// **'LANGUAGE'**
String get language;
/// No description provided for @statBestScore.
///
/// In en, this message translates to:
/// **'Best score'**
String get statBestScore;
/// No description provided for @statCaught.
///
/// In en, this message translates to:
/// **'Caught'**
String get statCaught;
/// No description provided for @statSeenLabel.
///
/// In en, this message translates to:
/// **'Seen'**
String get statSeenLabel;
/// No description provided for @statCompletion.
///
/// In en, this message translates to:
/// **'Completion'**
String get statCompletion;
/// No description provided for @navList.
///
/// In en, this message translates to:
/// **'LIST'**
String get navList;
/// No description provided for @navGuess.
///
/// In en, this message translates to:
/// **'GUESS'**
String get navGuess;
/// No description provided for @navSystem.
///
/// In en, this message translates to:
/// **'SYSTEM'**
String get navSystem;
/// No description provided for @langEnglish.
///
/// In en, this message translates to:
/// **'English'**
String get langEnglish;
/// No description provided for @langFrench.
///
/// In en, this message translates to:
/// **'French'**
String get langFrench;
/// No description provided for @palette0.
///
/// In en, this message translates to:
/// **'Pokédex Red'**
String get palette0;
/// No description provided for @palette1.
///
/// In en, this message translates to:
/// **'Blue Ocean'**
String get palette1;
/// No description provided for @palette2.
///
/// In en, this message translates to:
/// **'Green Forest'**
String get palette2;
/// No description provided for @palette3.
///
/// In en, this message translates to:
/// **'Lightning Yellow'**
String get palette3;
/// No description provided for @palette4.
///
/// In en, this message translates to:
/// **'Purple Shadow'**
String get palette4;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'fr'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'fr':
return AppLocalizationsFr();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
}
+181
View File
@@ -0,0 +1,181 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'Pokéguess';
@override
String get whosThatPokemon => 'WHO\'S THAT POKÉMON?';
@override
String get shinyDetected => '✨ SHINY POKÉMON DETECTED! ✨';
@override
String get genFilter => 'GEN FILTER';
@override
String get identificationInput => 'IDENTIFICATION INPUT';
@override
String get hintPrefix => 'HINT';
@override
String get enterPokemonName => 'Enter Pokémon name...';
@override
String get continueButton => 'CONTINUE';
@override
String get guessButton => 'GUESS!';
@override
String hintButton(int count) {
return 'HINT ($count)';
}
@override
String skipButton(int count) {
return 'SKIP ($count)';
}
@override
String get currentScore => 'CURRENT SCORE';
@override
String personalBest(int score) {
return 'PERSONAL BEST: $score';
}
@override
String caughtShiny(String name) {
return '✨ SHINY! You caught $name! (+20 pts) ✨';
}
@override
String caughtNormal(String name) {
return 'Correct! You caught $name!';
}
@override
String get wrongGuess => 'Wrong guess! Try again.';
@override
String get errorLoadingPokemon => 'Error loading Pokémon';
@override
String get listNational => 'LIST - NATIONAL';
@override
String get tabAll => 'ALL';
@override
String get tabCaught => 'CAUGHT';
@override
String get pokemonDiscovered => 'POKEMON DISCOVERED';
@override
String noPokemonFound(String filter) {
return 'NO POKEMON FOUND IN $filter';
}
@override
String get pokedexFooter => 'NATIONAL POKEDEX V2.0';
@override
String get loadingError => 'Loading error';
@override
String get baseStats => 'BASE STATS';
@override
String get noDescription => 'No description available for this Pokémon.';
@override
String get gameOver => 'GAME OVER';
@override
String itWas(String name) {
return 'It was $name!';
}
@override
String get gameOverMessage =>
'\"Looks like your journey ends here. You\'ve run out of energy!\"';
@override
String get statStreak => 'STREAK';
@override
String get statSeen => 'SEEN';
@override
String get statScore => 'SCORE';
@override
String get tryAgain => 'TRY AGAIN';
@override
String get backToPokedex => 'BACK TO POKEDEX';
@override
String get system => 'SYSTEM';
@override
String get statistics => 'STATISTICS';
@override
String get colorPalette => 'COLOR PALETTE';
@override
String get language => 'LANGUAGE';
@override
String get statBestScore => 'Best score';
@override
String get statCaught => 'Caught';
@override
String get statSeenLabel => 'Seen';
@override
String get statCompletion => 'Completion';
@override
String get navList => 'LIST';
@override
String get navGuess => 'GUESS';
@override
String get navSystem => 'SYSTEM';
@override
String get langEnglish => 'English';
@override
String get langFrench => 'French';
@override
String get palette0 => 'Pokédex Red';
@override
String get palette1 => 'Blue Ocean';
@override
String get palette2 => 'Green Forest';
@override
String get palette3 => 'Lightning Yellow';
@override
String get palette4 => 'Purple Shadow';
}
+181
View File
@@ -0,0 +1,181 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'Pokéguess';
@override
String get whosThatPokemon => 'QUI EST CE POKÉMON ?';
@override
String get shinyDetected => '✨ POKÉMON SHINY DÉTECTÉ ! ✨';
@override
String get genFilter => 'FILTRE GÉN.';
@override
String get identificationInput => 'SAISIE D\'IDENTIFICATION';
@override
String get hintPrefix => 'INDICE';
@override
String get enterPokemonName => 'Entrez le nom du Pokémon...';
@override
String get continueButton => 'CONTINUER';
@override
String get guessButton => 'DEVINER !';
@override
String hintButton(int count) {
return 'INDICE ($count)';
}
@override
String skipButton(int count) {
return 'PASSER ($count)';
}
@override
String get currentScore => 'SCORE ACTUEL';
@override
String personalBest(int score) {
return 'MEILLEUR SCORE : $score';
}
@override
String caughtShiny(String name) {
return '✨ SHINY ! Tu as attrapé $name ! (+20 pts) ✨';
}
@override
String caughtNormal(String name) {
return 'Correct ! Tu as attrapé $name !';
}
@override
String get wrongGuess => 'Mauvaise réponse ! Réessaie.';
@override
String get errorLoadingPokemon => 'Erreur de chargement du Pokémon';
@override
String get listNational => 'LISTE - NATIONALE';
@override
String get tabAll => 'TOUS';
@override
String get tabCaught => 'ATTRAPÉS';
@override
String get pokemonDiscovered => 'POKÉMON DÉCOUVERTS';
@override
String noPokemonFound(String filter) {
return 'AUCUN POKÉMON DANS $filter';
}
@override
String get pokedexFooter => 'POKÉDEX NATIONAL V2.0';
@override
String get loadingError => 'Erreur de chargement';
@override
String get baseStats => 'STATS DE BASE';
@override
String get noDescription => 'Aucune description disponible pour ce Pokémon.';
@override
String get gameOver => 'PARTIE TERMINÉE';
@override
String itWas(String name) {
return 'C\'était $name !';
}
@override
String get gameOverMessage =>
'« On dirait que ton aventure s\'arrête ici. Tu n\'as plus d\'énergie ! »';
@override
String get statStreak => 'SÉRIE';
@override
String get statSeen => 'VUS';
@override
String get statScore => 'SCORE';
@override
String get tryAgain => 'REJOUER';
@override
String get backToPokedex => 'RETOUR AU POKÉDEX';
@override
String get system => 'SYSTÈME';
@override
String get statistics => 'STATISTIQUES';
@override
String get colorPalette => 'PALETTE DE COULEURS';
@override
String get language => 'LANGUE';
@override
String get statBestScore => 'Meilleur score';
@override
String get statCaught => 'Attrapés';
@override
String get statSeenLabel => 'Vus';
@override
String get statCompletion => 'Complétion';
@override
String get navList => 'LISTE';
@override
String get navGuess => 'DEVINER';
@override
String get navSystem => 'SYSTÈME';
@override
String get langEnglish => 'Anglais';
@override
String get langFrench => 'Français';
@override
String get palette0 => 'Pokédex Rouge';
@override
String get palette1 => 'Océan Bleu';
@override
String get palette2 => 'Forêt Verte';
@override
String get palette3 => 'Foudre Jaune';
@override
String get palette4 => 'Ombre Violette';
}
+35 -14
View File
@@ -1,9 +1,14 @@
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/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'l10n/app_localizations.dart';
import 'presentation/pages/main_page.dart';
import 'presentation/pages/pokemon_detail.dart';
import 'presentation/pages/game_over_page.dart';
import 'presentation/providers/theme_provider.dart';
import 'presentation/providers/locale_provider.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -11,27 +16,43 @@ void main() {
sqfliteFfiInit(); sqfliteFfiInit();
databaseFactory = databaseFactoryFfi; databaseFactory = databaseFactoryFfi;
} }
runApp(const MyApp()); runApp(const ProviderScope(child: MyApp()));
} }
class MyApp extends StatelessWidget { /// Widget racine : construit le MaterialApp (thème dérivé de la palette, routes).
class MyApp extends ConsumerWidget {
const MyApp({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex];
final locale = ref.watch(localeProvider);
return MaterialApp( return MaterialApp(
title: 'Pokéguess', // Titre de l'application onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorScheme: ColorScheme.fromSeed(
seedColor: palette.primary,
surface: palette.surface,
),
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(),
},
); );
} }
} }
-84
View File
@@ -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/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png';
String get shinyImageUrl => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/$id.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
}
-79
View File
@@ -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(),
],
)
]
)
),
);
}
}
-69
View File
@@ -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,
)
),
);
}
}
+111
View File
@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/repository_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/game_over/game_over_header.dart';
import '../widgets/game_over/game_over_stats.dart';
import '../widgets/game_over/game_over_actions.dart';
import '../widgets/game_over/hinge_divider.dart';
/// Écran de fin de partie. Affiche le Pokémon manqué, les stats, et propose de rejouer.
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 args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
final pokemonImage = args?['pokemonImage'] ?? '';
final pokemonName = args?['pokemonName'] ?? 'Unknown';
final int streak = args?['streak'] ?? 0;
final int score = args?['score'] ?? 0;
final palette = appPalettes[ref.watch(themeProvider)];
final primaryDark = HSLColor.fromColor(palette.primary)
.withLightness((HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0))
.toColor();
const silverBg = Color(0xFFC8D1D8);
return Scaffold(
backgroundColor: palette.primary,
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
GameOverHeader(
pokemonImage: pokemonImage,
pokemonName: pokemonName,
primaryDark: primaryDark,
surfaceColor: palette.surface,
),
HingeDivider(color: primaryDark),
Container(
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: silverBg,
child: Column(
children: [
Container(
width: double.infinity,
color: palette.surface,
padding: const EdgeInsets.all(24),
child: Text(
AppLocalizations.of(context)!.gameOverMessage,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
),
),
const SizedBox(height: 16),
GameOverStats(
streak: streak.toString().padLeft(3, '0'),
seen: "$_seenCount/${AppConstants.totalPokemon}",
score: "$score",
),
const SizedBox(height: 16),
GameOverActions(
primaryColor: palette.primary,
primaryDark: primaryDark,
onTryAgain: () => Navigator.pop(context, true),
onBack: () => Navigator.pop(context, false),
),
],
),
),
),
],
),
),
),
);
}
}
+153
View File
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/game/game_state.dart';
import '../../l10n/app_localizations.dart';
import '../providers/gen_filter_provider.dart';
import '../providers/game_provider.dart';
import '../providers/navigation_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/guess/guess_silhouette.dart';
import '../widgets/guess/gen_filter_section.dart';
import '../widgets/guess/lives_row.dart';
import '../widgets/guess/guess_input_section.dart';
import '../widgets/guess/score_board.dart';
/// Page de jeu "devine le Pokémon" : orchestre gameProvider et compose les sous-widgets.
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;
bool _genFilterOpen = false;
@override
void initState() {
super.initState();
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 l = AppLocalizations.of(context)!;
final state = ref.read(gameProvider);
switch (result) {
case GuessResult.correct:
final name = state.currentPokemon!.formatedName;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)),
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
));
break;
case GuessResult.wrong:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(l.wrongGuess), 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 / false / null (geste retour) relancent tous une partie pour ne pas rester bloqué.
await ref.read(gameProvider.notifier).startNewGame();
}
@override
Widget build(BuildContext context) {
final state = ref.watch(gameProvider);
final palette = appPalettes[ref.watch(themeProvider)];
if (state.status == GameStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.currentPokemon == null || state.status == GameStatus.error) {
return Center(child: Text(AppLocalizations.of(context)!.errorLoadingPokemon));
}
final pokemon = state.currentPokemon!;
final isGuessed = state.status == GameStatus.roundWon;
final notifier = ref.read(gameProvider.notifier);
return Container(
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
child: Stack(
children: [
const ScanlineOverlay(),
SingleChildScrollView(
child: Column(
children: [
GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed, surfaceColor: palette.surface),
GenFilterSection(
isOpen: _genFilterOpen,
onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen),
selectedGens: ref.watch(genFilterProvider),
onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i),
surfaceColor: palette.surface,
),
const SizedBox(height: 12),
LivesRow(lives: state.lives),
const SizedBox(height: 16),
GuessInputSection(
controller: _guessController,
pokemonName: pokemon.formatedName,
isHintUsed: state.isHintUsed,
isGuessed: isGuessed,
hints: state.hints,
skips: state.skips,
onGuess: _onGuess,
onContinue: () => notifier.loadNextPokemon(),
onHint: (state.isHintUsed || state.hints <= 0) ? null : () => notifier.useHint(),
onSkip: state.skips > 0 ? () => notifier.useSkip() : null,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ScoreBoard(currentScore: state.currentScore, bestScore: state.bestScore),
),
const SizedBox(height: 32),
],
),
),
],
),
);
}
}
+69
View File
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../l10n/app_localizations.dart';
import '../providers/navigation_provider.dart';
import '../providers/theme_provider.dart';
import 'pokemon_list.dart';
import 'guess_page.dart';
import 'system_page.dart';
/// Coquille principale : navigation par onglets (liste / jeu / système) via un IndexedStack.
class MainPage extends ConsumerWidget {
const MainPage({Key? key}) : super(key: key);
static const List<Widget> _pages = [
PokemonListPage(),
GuessPage(),
SystemPage(),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentIndex = ref.watch(selectedTabProvider);
final palette = appPalettes[ref.watch(themeProvider)];
final primaryDark = HSLColor.fromColor(palette.primary)
.withLightness((HSLColor.fromColor(palette.primary).lightness - 0.1).clamp(0.0, 1.0))
.toColor();
return Scaffold(
backgroundColor: palette.surface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: palette.primary,
borderRadius: BorderRadius.circular(30),
border: Border.all(color: primaryDark, 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: palette.primary,
unselectedItemColor: Colors.grey,
items: [
BottomNavigationBarItem(icon: const Icon(Icons.grid_view), label: AppLocalizations.of(context)!.navList),
BottomNavigationBarItem(icon: const Icon(Icons.games), label: AppLocalizations.of(context)!.navGuess),
BottomNavigationBarItem(icon: const Icon(Icons.settings), label: AppLocalizations.of(context)!.navSystem),
],
),
),
);
}
}
+128
View File
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart';
import '../providers/locale_provider.dart';
import '../providers/repository_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/detail/pokemon_detail_top.dart';
import '../widgets/detail/pokemon_stats_panel.dart';
/// Fiche détaillée d'un Pokémon (reçu via les arguments de route).
class PokemonDetailPage extends ConsumerStatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key);
@override
ConsumerState<PokemonDetailPage> createState() => _PokemonDetailPageState();
}
class _PokemonDetailPageState extends ConsumerState<PokemonDetailPage> {
bool _isShiny = false;
String? _englishGenus;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_fetchEnglishGenusIfNeeded();
}
Future<void> _fetchEnglishGenusIfNeeded() async {
final lang = ref.read(localeProvider).languageCode;
if (lang != 'en') return;
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
final genus = await ref.read(pokemonRepositoryProvider).getEnglishGenus(pokemon.id);
if (mounted && genus != null) setState(() => _englishGenus = genus);
}
@override
Widget build(BuildContext context) {
final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
final palette = appPalettes[ref.watch(themeProvider)];
final primaryDark = HSLColor.fromColor(palette.primary)
.withLightness(
(HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0))
.toColor();
return Scaffold(
backgroundColor: palette.surface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: palette.primary,
borderRadius: BorderRadius.circular(30),
border: Border.all(color: primaryDark, width: 4),
),
child: SingleChildScrollView(
child: Column(
children: [
_backBar(context, primaryDark),
PokemonDetailTop(
pokemon: pokemon,
isShiny: _isShiny,
onToggleShiny: () => setState(() => _isShiny = !_isShiny),
primaryColor: palette.primary,
primaryDark: primaryDark,
surfaceColor: palette.surface,
),
const SizedBox(height: 20),
_hinge(primaryDark),
const SizedBox(height: 20),
PokemonStatsPanel(
pokemon: pokemon,
surfaceColor: palette.surface,
descriptionOverride: _englishGenus,
),
const SizedBox(height: 30),
_bottomDots(primaryDark),
const SizedBox(height: 20),
],
),
),
),
),
),
);
}
Widget _backBar(BuildContext context, Color primaryDark) {
return 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: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
);
}
Widget _hinge(Color primaryDark) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(height: 6, width: 40, color: primaryDark),
Container(height: 6, width: 40, color: primaryDark),
],
);
}
Widget _bottomDots(Color primaryDark) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(i) => Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle),
),
),
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/pokemon_tile.dart';
import '../widgets/scanline_overlay.dart';
import '../widgets/list/pokedex_list_header.dart';
import '../widgets/list/pokedex_count_bar.dart';
/// Liste du Pokédex national avec filtre ALL / CAUGHT.
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 l = AppLocalizations.of(context)!;
final palette = appPalettes[ref.watch(themeProvider)];
final pokedexAsync = ref.watch(pokedexProvider);
final caughtCount = ref.watch(caughtCountProvider);
return Container(
decoration: const BoxDecoration(color: Color(0xFFC8D1D8)),
child: Column(
children: [
const PokedexListHeader(),
Container(
color: const Color(0xFF90A4AE),
height: 40,
child: Row(
children: [
_buildTab('ALL', l.tabAll, _filter == 'ALL', palette.primary),
_buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT', palette.primary),
],
),
),
PokedexCountBar(caught: caughtCount, total: pokedexAsync.valueOrNull?.length ?? 0),
Expanded(
child: Stack(
children: [
const ScanlineOverlay(),
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Text('${l.loadingError}\n$e',
textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
),
data: (all) {
final filtered = _applyFilter(all);
if (filtered.isEmpty) return _emptyState();
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(12),
itemCount: filtered.length,
itemBuilder: (context, index) => PokemonTile(filtered[index]),
);
},
),
],
),
),
Container(
height: 24,
color: palette.surface,
alignment: Alignment.center,
child: Text(l.pokedexFooter,
style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
),
],
),
);
}
Widget _emptyState() {
final l = AppLocalizations.of(context)!;
final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16),
Text(l.noPokemonFound(filterLabel),
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildTab(String key, String label, bool isSelected, Color primaryColor) {
return Expanded(
child: GestureDetector(
onTap: () {
if (_filter != key) {
setState(() => _filter = key);
if (_scrollController.hasClients) _scrollController.jumpTo(0);
}
},
child: Container(
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
border: isSelected
? Border(bottom: BorderSide(color: primaryColor, width: 3))
: null,
),
alignment: Alignment.center,
child: Text(label,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
),
),
);
}
}
+92
View File
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart';
import '../providers/locale_provider.dart';
import '../widgets/system/system_header.dart';
import '../widgets/system/section_title.dart';
import '../widgets/system/system_stats.dart';
import '../widgets/system/palette_picker.dart';
import '../widgets/system/language_picker.dart';
/// Page "Système" : statistiques de jeu, langue et palette de couleurs.
class SystemPage extends ConsumerWidget {
const SystemPage({super.key});
Future<int> _loadBestScore() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(AppConstants.prefsBestScore) ?? 0;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context)!;
final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex];
final pokedexAsync = ref.watch(pokedexProvider);
return Container(
color: const Color(0xFFC8D1D8),
child: Column(
children: [
SystemHeader(primaryColor: palette.primary, title: l.system),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionTitle(text: l.statistics, color: palette.primary),
const SizedBox(height: 8),
pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Text(l.loadingError),
data: (pokemons) {
final total = pokemons.length;
final caught = pokemons.where((p) => p.isCaught).length;
final seen = pokemons.where((p) => p.isSeen).length;
final pct = total > 0 ? (caught / total * 100).toStringAsFixed(1) : '0.0';
return FutureBuilder<int>(
future: _loadBestScore(),
builder: (context, snap) {
final best = snap.data ?? 0;
return StatsGrid(
primaryColor: palette.primary,
items: [
StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events),
StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon),
StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility),
StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart),
],
);
},
);
},
),
const SizedBox(height: 24),
SectionTitle(text: l.language, color: palette.primary),
const SizedBox(height: 8),
LanguagePicker(
selected: ref.watch(localeProvider),
primaryColor: palette.primary,
onSelect: (loc) => ref.read(localeProvider.notifier).setLocale(loc),
),
const SizedBox(height: 24),
SectionTitle(text: l.colorPalette, color: palette.primary),
const SizedBox(height: 8),
PalettePicker(
selectedIndex: paletteIndex,
onSelect: (i) => ref.read(themeProvider.notifier).setPalette(i),
),
],
),
),
),
],
),
);
}
}
@@ -0,0 +1,108 @@
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 'gen_filter_provider.dart';
import 'locale_provider.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() {
// Re-fetch the current Pokémon whenever the locale changes so the name
// (used for hints and the guess comparison) switches language instantly.
ref.listen(localeProvider, (_, __) => _reloadCurrentPokemon());
return const GameState();
}
Future<void> _reloadCurrentPokemon() async {
final current = state.currentPokemon;
if (current == null || state.status == GameStatus.loading) return;
try {
final updated = await ref.read(pokemonRepositoryProvider).getById(current.id);
if (updated != null) {
state = state.copyWith(
currentPokemon: updated.copyWith(
isCaught: current.isCaught,
isSeen: current.isSeen,
),
);
}
} catch (_) {}
}
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 genFilter = ref.read(genFilterProvider.notifier);
await genFilter.ready;
final id = genFilter.randomId(_random);
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);
@@ -0,0 +1,62 @@
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
/// Gère les générations activées pour le tirage et fournit un id aléatoire dans celles-ci.
/// La sélection est persistée dans les préférences.
class GenFilterNotifier extends Notifier<Set<int>> {
late final Future<void> _ready;
@override
Set<int> build() {
_ready = _load();
// Default: all gens enabled — overwritten by _load() once prefs are read.
return Set.from(List.generate(AppConstants.genRanges.length, (i) => i));
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getStringList(AppConstants.prefsGenFilter);
if (saved != null && saved.isNotEmpty) {
state = saved.map(int.parse).toSet();
}
}
/// Resolves once the persisted filter has been loaded from SharedPreferences.
Future<void> get ready => _ready;
Future<void> toggle(int genIndex) async {
final next = Set<int>.from(state);
if (next.contains(genIndex)) {
// Prevent deselecting the last gen
if (next.length == 1) return;
next.remove(genIndex);
} else {
next.add(genIndex);
}
state = next;
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
AppConstants.prefsGenFilter,
state.map((i) => i.toString()).toList(),
);
}
/// Returns a random Pokémon ID within the currently enabled gens.
int randomId(Random random) {
final ranges = state.map((i) => AppConstants.genRanges[i]).toList();
final totalPool = ranges.fold(0, (sum, r) => sum + (r.$2 - r.$1 + 1));
var pick = random.nextInt(totalPool);
for (final r in ranges) {
final size = r.$2 - r.$1 + 1;
if (pick < size) return r.$1 + pick;
pick -= size;
}
return 1;
}
}
/// Ensemble des index de générations actuellement activées pour le jeu.
final genFilterProvider =
NotifierProvider<GenFilterNotifier, Set<int>>(GenFilterNotifier.new);
@@ -0,0 +1,33 @@
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart';
/// Langues prises en charge par l'application.
const List<Locale> supportedLocales = [Locale('en'), Locale('fr')];
/// Gère la langue sélectionnée, persistée dans les préférences.
class LocaleNotifier extends Notifier<Locale> {
@override
Locale build() {
_loadSaved();
return const Locale('fr');
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString(AppConstants.prefsLocale);
if (code != null && supportedLocales.any((l) => l.languageCode == code)) {
state = Locale(code);
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(AppConstants.prefsLocale, locale.languageCode);
}
}
/// Langue active de l'application.
final localeProvider = NotifierProvider<LocaleNotifier, Locale>(LocaleNotifier.new);
@@ -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);
@@ -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.watch(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,
);
});
@@ -0,0 +1,17 @@
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';
import 'locale_provider.dart';
/// Point d'injection unique du repository.
/// Se reconstruit automatiquement quand la langue change,
/// ce qui invalide [pokedexProvider] en cascade.
final pokemonRepositoryProvider = Provider<PokemonRepository>((ref) {
final lang = ref.watch(localeProvider).languageCode;
final remote = PokemonRemoteDataSource(languageCode: lang);
final local = kIsWeb ? null : PokemonLocalDataSource(languageCode: lang);
return PokemonRepositoryImpl(remote: remote, local: local);
});
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Palette de couleurs de l'application (couleur primaire + surface de fond).
/// Le nom affiché est géré via AppLocalizations (palette0…palette4).
class AppPalette {
final Color primary;
final Color surface;
const AppPalette({required this.primary, required this.surface});
}
const List<AppPalette> appPalettes = [
AppPalette(primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)),
AppPalette(primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)),
AppPalette(primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)),
AppPalette(primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)),
AppPalette(primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)),
];
const String _prefsPaletteIndex = 'palette_index';
/// Gère l'index de la palette sélectionnée, persistée dans les préférences.
class ThemeNotifier extends Notifier<int> {
@override
int build() {
_loadSaved();
return 0;
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final idx = prefs.getInt(_prefsPaletteIndex) ?? 0;
state = idx.clamp(0, appPalettes.length - 1);
}
Future<void> setPalette(int index) async {
state = index.clamp(0, appPalettes.length - 1);
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefsPaletteIndex, state);
}
AppPalette get current => appPalettes[state];
}
/// Index de la palette de couleurs active.
final themeProvider = NotifierProvider<ThemeNotifier, int>(ThemeNotifier.new);
+64
View File
@@ -0,0 +1,64 @@
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 localisé (EN par défaut, FR si languageCode == 'fr').
String localizedTypeName(PokemonType type, String languageCode) {
if (languageCode == 'fr') {
const fr = {
PokemonType.normal: 'Normal',
PokemonType.fighting: 'Combat',
PokemonType.flying: 'Vol',
PokemonType.poison: 'Poison',
PokemonType.ground: 'Sol',
PokemonType.rock: 'Roche',
PokemonType.bug: 'Insecte',
PokemonType.ghost: 'Spectre',
PokemonType.steel: 'Acier',
PokemonType.fire: 'Feu',
PokemonType.water: 'Eau',
PokemonType.grass: 'Plante',
PokemonType.electric: 'Électrik',
PokemonType.psychic: 'Psy',
PokemonType.ice: 'Glace',
PokemonType.dragon: 'Dragon',
PokemonType.dark: 'Ténèbres',
PokemonType.fairy: 'Fée',
PokemonType.unknown: '?',
PokemonType.shadow: '?',
};
return fr[type] ?? '?';
}
// English: capitalise the enum name
final name = type.name;
return name[0].toUpperCase() + name.substring(1);
}
/// Nom du type avec une majuscule initiale (anglais, conservé pour compatibilité).
String formatedTypeName(PokemonType type) => localizedTypeName(type, 'en');
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
import '../pokemon_type.dart';
import '../pokemon_image.dart';
/// Écran supérieur du détail : numéro, sprite (tap pour basculer shiny), nom et types.
class PokemonDetailTop extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final VoidCallback onToggleShiny;
final Color primaryColor;
final Color primaryDark;
final Color surfaceColor;
const PokemonDetailTop({
super.key,
required this.pokemon,
required this.isShiny,
required this.onToggleShiny,
required this.primaryColor,
required this.primaryDark,
required this.surfaceColor,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: surfaceColor, width: 8),
),
child: Container(
color: const Color(0xFF90A4AE),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: surfaceColor,
child: Text(
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
GestureDetector(
onTap: onToggleShiny,
child: Container(
height: 180,
alignment: Alignment.center,
color: const Color(0xFF81CCA5).withAlpha(153),
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!),
],
),
],
),
),
],
),
),
);
}
}
@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../domain/entities/pokemon.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/locale_provider.dart';
/// Écran inférieur du détail : stats de base, description localisée et éléments décoratifs.
class PokemonStatsPanel extends ConsumerWidget {
final Pokemon pokemon;
final Color surfaceColor;
final String? descriptionOverride;
const PokemonStatsPanel({
super.key,
required this.pokemon,
required this.surfaceColor,
this.descriptionOverride,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context)!;
ref.watch(localeProvider); // watch locale so widget rebuilds on language change
final description = descriptionOverride ?? pokemon.description;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: surfaceColor,
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: [
Text(l.baseStats,
style: const 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),
_StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)),
_StatBar(label: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)),
_StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)),
_StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE2EBF0),
border: Border.all(color: Colors.grey[400]!),
),
child: Text(
description != null && description.isNotEmpty
? '"$description"'
: '"${l.noDescription}"',
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 16),
_DecorativeLights(accentColor: surfaceColor),
],
),
),
);
}
}
class _StatBar extends StatelessWidget {
final String label;
final int value;
final Color color;
const _StatBar({required this.label, required this.value, required this.color});
@override
Widget build(BuildContext context) {
final 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),
),
],
),
);
}
}
class _DecorativeLights extends StatelessWidget {
final Color accentColor;
const _DecorativeLights({required this.accentColor});
@override
Widget build(BuildContext context) {
final dark = HSLColor.fromColor(accentColor)
.withLightness(
(HSLColor.fromColor(accentColor).lightness - 0.1).clamp(0.0, 1.0))
.toColor();
return Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: accentColor,
shape: BoxShape.circle,
border: Border.all(color: dark, 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))),
],
),
],
);
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
class GameOverActions extends StatelessWidget {
final Color primaryColor;
final Color primaryDark;
final VoidCallback onTryAgain;
final VoidCallback onBack;
const GameOverActions({
super.key,
required this.primaryColor,
required this.primaryDark,
required this.onTryAgain,
required this.onBack,
});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Column(
children: [
_button(label: l.tryAgain, icon: Icons.refresh, color: primaryColor, onPressed: onTryAgain),
const SizedBox(height: 16),
_button(label: l.backToPokedex, icon: Icons.menu_book, color: primaryDark, onPressed: onBack),
],
);
}
Widget _button({
required String label,
required IconData icon,
required Color color,
required VoidCallback onPressed,
}) {
return SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: Colors.white, size: 24),
label: Text(
label,
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2),
),
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
);
}
}
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart';
/// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon.
class GameOverHeader extends StatelessWidget {
final String pokemonImage;
final String pokemonName;
final Color primaryDark;
final Color surfaceColor;
const GameOverHeader({
super.key,
required this.pokemonImage,
required this.pokemonName,
required this.primaryDark,
required this.surfaceColor,
});
static const _silverBg = Color(0xFFC8D1D8);
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container(
decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
color: _silverBg,
child: Column(
children: [
Container(
color: primaryDark,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text(
l.gameOver,
style: const 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),
if (pokemonImage.isNotEmpty)
SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)),
const SizedBox(height: 12),
Text(
l.itWas(pokemonName),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: surfaceColor,
letterSpacing: 1,
),
),
const SizedBox(height: 8),
],
),
),
);
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE).
class GameOverStats extends StatelessWidget {
final String streak;
final String seen;
final String score;
const GameOverStats({super.key, required this.streak, required this.seen, required this.score});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Row(
children: [
Expanded(child: _StatBox(label: l.statStreak, value: streak)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: l.statSeen, value: seen)),
const SizedBox(width: 16),
Expanded(child: _StatBox(label: l.statScore, value: score)),
],
);
}
}
/// Une case de statistique : un libellé rouge au-dessus d'une valeur.
class _StatBox extends StatelessWidget {
final String label;
final String value;
const _StatBox({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFFD9E0E5),
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
Text(
label,
style: const TextStyle(color: Colors.red, fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
}
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
/// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over.
class HingeDivider extends StatelessWidget {
final Color color;
const HingeDivider({super.key, required this.color});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Row(
children: [
Expanded(child: Container(height: 2, color: color)),
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: BoxDecoration(color: color, shape: BoxShape.circle),
),
),
),
const SizedBox(width: 8),
Expanded(child: Container(height: 2, color: color)),
],
),
);
}
}
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.dart';
import '../../../l10n/app_localizations.dart';
/// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations.
class GenFilterSection extends StatelessWidget {
final bool isOpen;
final VoidCallback onToggleOpen;
final Set<int> selectedGens;
final void Function(int) onToggle;
final Color surfaceColor;
const GenFilterSection({
super.key,
required this.isOpen,
required this.onToggleOpen,
required this.selectedGens,
required this.onToggle,
required this.surfaceColor,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: onToggleOpen,
icon: Icon(
isOpen ? Icons.expand_less : Icons.filter_list,
color: surfaceColor,
),
label: Text(
AppLocalizations.of(context)!.genFilter,
style: TextStyle(
color: surfaceColor,
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 2,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: surfaceColor, width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: isOpen
? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle, surfaceColor: surfaceColor)
: const SizedBox.shrink(),
),
],
),
);
}
}
class _GenFilterPanel extends StatelessWidget {
final Set<int> selectedGens;
final void Function(int) onToggle;
final Color surfaceColor;
const _GenFilterPanel({required this.selectedGens, required this.onToggle, required this.surfaceColor});
static const _genNames = [
'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'
];
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: surfaceColor, width: 2),
),
child: Column(
children: List.generate(AppConstants.genRanges.length, (i) {
final range = AppConstants.genRanges[i];
final isSelected = selectedGens.contains(i);
final isLast = i == AppConstants.genRanges.length - 1;
return InkWell(
onTap: () => onToggle(i),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF1B2333).withAlpha(12) : Colors.transparent,
border: isLast ? null : Border(bottom: BorderSide(color: Colors.grey.shade200)),
),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 20,
height: 20,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.transparent,
border: Border.all(
color: isSelected ? const Color(0xFF3B6EE3) : Colors.grey.shade400,
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
child: isSelected ? const Icon(Icons.check, size: 13, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_genNames[i],
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? const Color(0xFF1B2333) : Colors.black54,
),
),
),
Text(
'#${range.$1}#${range.$2}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
);
}),
),
);
}
}
@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Section de saisie : indice optionnel, champ de réponse et boutons d'action
/// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks.
class GuessInputSection extends StatelessWidget {
final TextEditingController controller;
final String pokemonName;
final bool isHintUsed;
final bool isGuessed;
final int hints;
final int skips;
final VoidCallback onGuess;
final VoidCallback onContinue;
final VoidCallback? onHint;
final VoidCallback? onSkip;
const GuessInputSection({
super.key,
required this.controller,
required this.pokemonName,
required this.isHintUsed,
required this.isGuessed,
required this.hints,
required this.skips,
required this.onGuess,
required this.onContinue,
required this.onHint,
required this.onSkip,
});
/// Masque le nom en ne laissant visibles que la première et la dernière lettre.
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 l = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.identificationInput,
style: const TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
if (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(
"${l.hintPrefix}: ${_maskedName(pokemonName)}",
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: controller,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: l.enterPokemonName,
),
onSubmitted: (_) => onGuess(),
),
),
const SizedBox(height: 16),
if (isGuessed)
_bigButton(label: l.continueButton, color: Colors.green, onPressed: onContinue)
else ...[
_bigButton(label: l.guessButton, color: const Color(0xFF3B6EE3), onPressed: onGuess),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _actionButton(icon: Icons.lightbulb, label: l.hintButton(hints), color: Colors.amber, onPressed: onHint),
),
const SizedBox(width: 8),
Expanded(
child: _actionButton(icon: Icons.skip_next, label: l.skipButton(skips), color: Colors.grey[400]!, onPressed: onSkip),
),
],
),
],
],
),
);
}
Widget _bigButton({required String label, required Color color, required VoidCallback onPressed}) {
return SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: Text(
label,
style: const TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
);
}
Widget _actionButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback? onPressed,
}) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: Colors.black87),
label: Text(label, style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
);
}
}
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart';
/// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée).
class GuessSilhouette extends StatelessWidget {
final Pokemon pokemon;
final bool isShiny;
final bool isGuessed;
final Color surfaceColor;
const GuessSilhouette({
super.key,
required this.pokemon,
required this.isShiny,
required this.isGuessed,
required this.surfaceColor,
});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl;
return 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: surfaceColor, width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: isGuessed
? PokemonImage(
imageUrl: imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
)
: PokemonImage(
imageUrl: imageUrl,
fallbackUrl: pokemon.imageUrl,
fit: BoxFit.contain,
color: isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
),
),
),
Container(
color: surfaceColor,
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
isShiny ? l.shinyDetected : l.whosThatPokemon,
textAlign: TextAlign.center,
style: TextStyle(
color: isShiny ? Colors.yellow[400] : Colors.white,
fontSize: isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
),
],
),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import '../../../core/config/app_constants.dart';
/// Rangée de cœurs représentant les vies restantes.
class LivesRow extends StatelessWidget {
final int lives;
const LivesRow({super.key, required this.lives});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(AppConstants.startingLives, (index) {
return Icon(
index < lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
);
}
}
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Encart affichant le score courant et le meilleur score personnel.
class ScoreBoard extends StatelessWidget {
final int currentScore;
final int bestScore;
const ScoreBoard({super.key, required this.currentScore, required this.bestScore});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return 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: [
Text(
l.currentScore,
style: const TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
"$currentScore",
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
),
const Divider(height: 24),
Text(
l.personalBest(bestScore),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
),
);
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Bandeau affichant le nombre de Pokémon découverts sur le total.
class PokedexCountBar extends StatelessWidget {
final int caught;
final int total;
const PokedexCountBar({super.key, required this.caught, required this.total});
@override
Widget build(BuildContext context) {
return 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(
'${caught.toString().padLeft(3, '0')} / $total',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
Text(AppLocalizations.of(context)!.pokemonDiscovered,
style: const TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
],
),
);
}
}
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Barre de titre de la liste du Pokédex.
class PokedexListHeader extends StatelessWidget {
const PokedexListHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
alignment: Alignment.center,
child: Text(AppLocalizations.of(context)!.listNational,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
);
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
/// Image réseau d'un Pokémon, avec URL de repli et placeholder en cas d'échec de chargement.
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,
),
),
],
),
);
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import '../../domain/entities/pokemon.dart';
import 'pokemon_image.dart';
/// Ligne de liste d'un Pokémon : image et nom si attrapé, masqué (???) sinon.
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,10 +1,8 @@
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 /// Pastille colorée affichant le nom d'un type de Pokémon (couleur de fond selon le type).
// Elle prend en paramètre un type de Pokémon
// Elle affiche le nom du type avec une couleur de fond correspondant au type
class PokemonTypeWidget extends StatelessWidget { class PokemonTypeWidget extends StatelessWidget {
const PokemonTypeWidget(this.type, {Key? key}) : super(key: key); const PokemonTypeWidget(this.type, {Key? key}) : super(key: key);
@@ -12,12 +10,13 @@ class PokemonTypeWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String typeName = formatedTypeName(type); final lang = Localizations.localeOf(context).languageCode;
String typeName = localizedTypeName(type, lang);
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,
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
/// Effet rétro de scanlines superposé en fond. À placer directement dans un [Stack].
class ScanlineOverlay extends StatelessWidget {
const ScanlineOverlay({super.key});
@override
Widget build(BuildContext context) {
return 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),
),
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/locale_provider.dart';
/// Sélecteur de langue de l'application (parmi [supportedLocales]).
class LanguagePicker extends StatelessWidget {
final Locale selected;
final Color primaryColor;
final void Function(Locale) onSelect;
const LanguagePicker({
super.key,
required this.selected,
required this.primaryColor,
required this.onSelect,
});
static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'};
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final names = {'en': l.langEnglish, 'fr': l.langFrench};
return Column(
children: supportedLocales.map((locale) {
final isSelected = locale.languageCode == selected.languageCode;
return GestureDetector(
onTap: () => onSelect(locale),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? primaryColor : Colors.grey.shade300,
width: isSelected ? 3 : 1.5,
),
),
child: Row(
children: [
Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)),
const SizedBox(width: 16),
Text(
names[locale.languageCode] ?? locale.languageCode,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.black87,
),
),
const Spacer(),
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
],
),
),
);
}).toList(),
);
}
}
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../../providers/theme_provider.dart';
/// Sélecteur de palette de couleurs de l'application.
class PalettePicker extends StatelessWidget {
final int selectedIndex;
final void Function(int) onSelect;
const PalettePicker({super.key, required this.selectedIndex, required this.onSelect});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final paletteNames = [l.palette0, l.palette1, l.palette2, l.palette3, l.palette4];
return Column(
children: List.generate(appPalettes.length, (i) {
final p = appPalettes[i];
final isSelected = i == selectedIndex;
return GestureDetector(
onTap: () => onSelect(i),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? p.primary : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? p.primary : Colors.grey.shade300,
width: isSelected ? 3 : 1.5,
),
boxShadow: isSelected
? [BoxShadow(color: p.primary.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3))]
: [],
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: p.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: p.surface,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
const SizedBox(width: 16),
Text(
paletteNames[i],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.black87,
),
),
const Spacer(),
if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22),
],
),
),
);
}),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
/// Titre de section avec une barre verticale colorée à gauche.
class SectionTitle extends StatelessWidget {
final String text;
final Color color;
const SectionTitle({super.key, required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(width: 4, height: 20, color: color),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color, letterSpacing: 2),
),
],
);
}
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
/// En-tête de la page Système.
class SystemHeader extends StatelessWidget {
final Color primaryColor;
final String title;
const SystemHeader({super.key, required this.primaryColor, required this.title});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: primaryColor,
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
),
child: Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
/// Donnée d'une statistique affichée dans la grille.
class StatItem {
final String label;
final String value;
final IconData icon;
const StatItem({required this.label, required this.value, required this.icon});
}
/// Grille 2 colonnes de cartes de statistiques.
class StatsGrid extends StatelessWidget {
final Color primaryColor;
final List<StatItem> items;
const StatsGrid({super.key, required this.primaryColor, required this.items});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: items.map((item) => _StatCard(item: item, color: primaryColor)).toList(),
);
}
}
class _StatCard extends StatelessWidget {
final StatItem item;
final Color color;
const _StatCard({required this.item, required this.color});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withAlpha(80), width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, 2)),
],
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, color: color, size: 28),
const SizedBox(height: 6),
Text(item.value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color)),
Text(
item.label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
);
}
}
-35
View File
@@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
// 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);
}
+1
View File
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
@@ -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 -1
View File
@@ -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
View 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
+101 -5
View File
@@ -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">
+3
View File
@@ -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>
+5 -1
View File
@@ -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
}
} }
+2
View File
@@ -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>
+2
View File
@@ -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>
+9
View File
@@ -11,6 +11,11 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# Internationalisation (FR / EN)
flutter_localizations:
sdk: flutter
intl: any
# Shared API # Shared API
sqflite_common: ^2.5.0 sqflite_common: ^2.5.0
@@ -24,6 +29,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:
@@ -32,3 +40,4 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
generate: true # active la génération des localisations (gen-l10n)

Some files were not shown because too many files have changed in this diff Show More