From a59ee26aaaa8293c0fc04af0c4c2c693f8e329ec Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Mon, 2 Feb 2026 12:00:50 +0100 Subject: [PATCH 1/2] chore: add CocoaPods support and update project configuration --- macos/Podfile.lock | 33 ++++++ macos/Runner.xcodeproj/project.pbxproj | 100 +++++++++++++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 3 +- .../contents.xcworkspacedata | 3 + macos/Runner/AppDelegate.swift | 6 +- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 macos/Podfile.lock diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..03b0e59 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,33 @@ +PODS: + - FlutterMacOS (1.0.0) + - FMDB (2.7.12): + - FMDB/standard (= 2.7.12) + - FMDB/Core (2.7.12) + - FMDB/standard (2.7.12): + - FMDB/Core + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 + sqflite: c73556b2499b92f0b6e6946abe4a4084510cdf90 + +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 39dd14a..379252f 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,12 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 2F63F588D69E6E84EF178DDE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38A997E4AF06585806D63A93 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + BC0787830DD8D138B3AFA3FE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ADD6793C62B3D4B8AF3C642 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1ADD6793C62B3D4B8AF3C642 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; 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 = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 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 = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 38A997E4AF06585806D63A93 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8425D271D17EE83BF167A836 /* 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 = ""; }; + 86FC039ADCC2CFF33032C226 /* 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A60151594AB2579C4697BCA2 /* 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 = ""; }; + AE1BE7E94A2D9EB80387B250 /* 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 = ""; }; + E791602D5E2611FF301C4F22 /* 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 = ""; }; + FACE8614944708AF3373EFE5 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2F63F588D69E6E84EF178DDE /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BC0787830DD8D138B3AFA3FE /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + B9231BE3DD2D9C2F0FE7683B /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + B9231BE3DD2D9C2F0FE7683B /* Pods */ = { + isa = PBXGroup; + children = ( + A60151594AB2579C4697BCA2 /* Pods-Runner.debug.xcconfig */, + 8425D271D17EE83BF167A836 /* Pods-Runner.release.xcconfig */, + 86FC039ADCC2CFF33032C226 /* Pods-Runner.profile.xcconfig */, + E791602D5E2611FF301C4F22 /* Pods-RunnerTests.debug.xcconfig */, + FACE8614944708AF3373EFE5 /* Pods-RunnerTests.release.xcconfig */, + AE1BE7E94A2D9EB80387B250 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 1ADD6793C62B3D4B8AF3C642 /* Pods_Runner.framework */, + 38A997E4AF06585806D63A93 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 6E388177BD91FCED76707214 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 29B2F3029551E8A809B61379 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + C7326370B9BF34B0BE156E06 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -227,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -290,6 +322,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 29B2F3029551E8A809B61379 /* [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; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -328,6 +382,45 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 6E388177BD91FCED76707214 /* [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; + }; + C7326370B9BF34B0BE156E06 /* [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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E791602D5E2611FF301C4F22 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -393,6 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FACE8614944708AF3373EFE5 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -407,6 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = AE1BE7E94A2D9EB80387B250 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index cc8fad0..c615ef4 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef64..b3c1761 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } -- 2.49.1 From 5d8f609e1893f29783177471db071fed45a055af Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Mon, 2 Feb 2026 15:56:14 +0100 Subject: [PATCH 2/2] chore: vibe coding test --- docs/ARCHITECTURE.md | 192 ++++++++++ ios/Runner.xcodeproj/project.pbxproj | 10 +- ios/Runner/Info.plist | 17 +- lib/api/pokemon_api.dart | 27 +- lib/main.dart | 15 +- lib/models/pokemon.dart | 4 +- lib/pages/game_page.dart | 519 +++++++++++++++++++++++++++ lib/pages/home_page.dart | 135 +++++++ lib/pages/pokemon_list.dart | 34 +- lib/utils/pokemon_type.dart | 25 ++ 10 files changed, 937 insertions(+), 41 deletions(-) create mode 100644 docs/ARCHITECTURE.md create mode 100644 lib/pages/game_page.dart create mode 100644 lib/pages/home_page.dart diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..3392a62 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,192 @@ +# Architecture du Projet - Pokéguess + +## Vue d'ensemble + +Application Flutter affichant les 151 premiers Pokémon avec noms en français, via l'API Tyradex et cache local SQLite. + +### Informations + +- **Version** : 1.0.0+1 +- **SDK** : Flutter >=3.1.0 <4.0.0 +- **Plateformes** : Android, iOS, Web, macOS, Linux, Windows + +## Architecture Générale + +L'application suit une architecture en couches (layered architecture) avec une séparation claire des responsabilités : + +```txt +┌─────────────────────────────────────────┐ +│ Couche Présentation │ +│ (Pages & Components/Widgets) │ +├─────────────────────────────────────────┤ +│ Couche Métier │ +│ (Models) │ +├─────────────────────────────────────────┤ +│ Couche Services │ +│ (API & Database & Utils) │ +└─────────────────────────────────────────┘ +``` + +## Structure du Projet + +```md +lib/ +├── main.dart # Point d'entrée de l'application +├── api/ +│ └── pokemon_api.dart # Service d'accès à l'API Tyradex +├── components/ +│ ├── pokemon_tile.dart # Widget de tuile pour afficher un Pokémon +│ └── pokemon_type.dart # Widget pour afficher le type d'un Pokémon +├── database/ +│ └── pokedex_database.dart # Gestion de la base de données SQLite +├── models/ +│ └── pokemon.dart # Modèle de données Pokémon +├── pages/ +│ ├── pokemon_detail.dart # Page de détail d'un Pokémon +│ └── pokemon_list.dart # Page de liste des Pokémon +└── utils/ + └── pokemon_type.dart # Utilitaires pour les types de Pokémon +``` + +## Composants Principaux + +### 1. Point d'Entrée + +[main.dart](../lib/main.dart) - Initialisation de l'application + +- Thème Material Design 3 +- Routes : `/` (liste) et `/pokemon-detail` (détail) + +### 2. Couche Présentation + +#### Pages + +##### PokemonListPage ([pokemon_list.dart](../lib/pages/pokemon_list.dart)) + +StatefulWidget affichant une grille de 151 Pokémon (2 colonnes mobile, 4 web) via `GridView.builder` et `FutureBuilder`. + +##### PokemonDetailPage ([pokemon_detail.dart](../lib/pages/pokemon_detail.dart)) + +StatefulWidget affichant les détails d'un Pokémon avec toggle shiny (`_isShiny`) via `GestureDetector`. + +#### Components + +##### PokemonTile ([pokemon_tile.dart](../lib/components/pokemon_tile.dart)) + +StatefulWidget affichant une carte Pokémon (image + nom) avec navigation vers détail au tap. + +##### PokemonTypeWidget ([pokemon_type.dart](../lib/components/pokemon_type.dart)) + +StatelessWidget affichant un badge coloré par type. + +### 3. Couche Modèle + +#### Pokemon ([pokemon.dart](../lib/models/pokemon.dart)) + +- **Propriétés** : + - `name` : String - Nom du Pokémon + - `id` : int - Numéro du Pokémon + - `type1` : PokemonType - Type principal + - `type2` : PokemonType? - Type secondaire (optionnel) + +- **Propriétés calculées** : `imageUrl`, `shinyImageUrl`, `cryUrl`, `formatedName`, `type1Color`, `type2Color`, `type1Formated`, `type2Formated` +- **Méthodes** : `fromJson()`, `toJson()`, `fromID()` (cache + API) +- **Enum** : `PokemonType` (20 types) + +### 4. Couche Services + +#### PokemonApi ([pokemon_api.dart](../lib/api/pokemon_api.dart)) + +Communication avec Tyradex API () + +- `getPokemon(int id)` : Récupère un Pokémon depuis l'API avec nom en français +- Exception levée si code HTTP ≠ 200 + +#### PokedexDatabase ([pokedex_database.dart](../lib/database/pokedex_database.dart)) + +Gestion SQLite locale (CRUD complet) + +- **Schéma** : `pokemon (id, name, type1, type2)` +- **Méthodes** : `initDatabase()`, `getDatabase()`, `insertPokemon()`, `getPokemonList()`, `getPokemon()`, `deletePokemon()`, `deleteAllPokemon()`, `updatePokemon()` +- Non disponible sur Web (`kIsWeb`) + +#### Utils ([pokemon_type.dart](../lib/utils/pokemon_type.dart)) + +- `frenchTypeToEnum()` : Conversion types français (API) vers enum +- `typeToColor()` : Mapping type → couleur +- `formatedTypeName()` : Formatage nom de type + +## Flux de Données + +### Récupération d'un Pokémon + +```md +1. PokemonListPage demande Pokemon.fromID(id) + ↓ +2. Vérification dans PokedexDatabase (si non-web) + ↓ +3a. Si trouvé → Retour du Pokémon en cache + ↓ +3b. Si non trouvé → Appel à PokemonApi.getPokemon(id) + ↓ +4. Parsing JSON et création de l'objet Pokemon + ↓ +5. Sauvegarde dans PokedexDatabase (si non-web) + ↓ +6. Retour du Pokemon à la vue +``` + +### Navigation + +```md +PokemonListPage + ↓ (tap sur PokemonTile) +Navigator.pushNamed('/pokemon-detail', arguments: pokemon) + ↓ +PokemonDetailPage (récupère le pokemon via ModalRoute) +``` + +## Patterns et Bonnes Pratiques + +**Patterns** : Repository (classe `Pokemon`), Factory (`fromJson()`), FutureBuilder (async) + +**Plateformes** : Détection Web via `kIsWeb`, adaptation colonnes grille + +**Erreurs** : Try/catch + logs, vérification HTTP, messages dans FutureBuilder + +## Dépendances + +**Production** : `flutter`, `cupertino_icons` (^1.0.2), `sqflite` (^2.3.0), `http` (^1.1.0) + +**Développement** : `flutter_test`, `flutter_lints` (^2.0.0) + +## Points d'Extension + +1. Recherche et filtres (type, génération) +2. Mode jeu (deviner à partir de silhouette, score) +3. Système de favoris +4. Lecture audio des cris +5. Détails enrichis (stats, évolutions, capacités) +6. Préchargement offline des 151 Pokémon +7. Tests unitaires et d'intégration + +## Limitations + +1. Pas de persistance locale sur Web +2. Limité à 151 Pokémon (Gen 1) +3. Gestion d'erreur minimale +4. Pas d'internationalisation +5. Pas de tests automatisés + +## Notes Techniques + +- Material Design 3 +- Images : repository Yarkis01/TyraDex (GitHub) +- API : Tyradex API (https://tyradex.vercel.app) +- Noms de Pokémon en français +- Routage : routes nommées Flutter +- Pas de state management externe + +--- + +**Dernière mise à jour** : 2 février 2026 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 34e0dea..25d8c8c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -114,7 +114,6 @@ 379611D51312FF940696FCAB /* Pods-RunnerTests.release.xcconfig */, 0572F0648954163DB68882FD /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -469,13 +468,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 45K9ZX66MZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.pokedex; + PRODUCT_BUNDLE_IDENTIFIER = com.example.pokeguess; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -647,13 +647,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 45K9ZX66MZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.pokedex; + PRODUCT_BUNDLE_IDENTIFIER = com.example.pokeguess; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -669,13 +670,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 45K9ZX66MZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.pokedex; + PRODUCT_BUNDLE_IDENTIFIER = com.example.pokeguess; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f3ff95b..a153efc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,17 @@ + NSLocalNetworkUsageDescription + This app uses the local network to discover and connect to nearby devices on your network. + + NSBonjourServices + + + _http._tcp. + + + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +35,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +54,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/lib/api/pokemon_api.dart b/lib/api/pokemon_api.dart index 98236b9..a13b6c3 100644 --- a/lib/api/pokemon_api.dart +++ b/lib/api/pokemon_api.dart @@ -1,18 +1,18 @@ import '../models/pokemon.dart'; +import '../utils/pokemon_type.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; -// Classe qui permet de récupérer les données des pokémons depuis l'API +// Classe qui permet de récupérer les données des pokémons depuis l'API Tyradex // On utilise la librairie http pour effectuer les requêtes // On utilise la librairie dart:convert pour convertir les données JSON en objet Dart class PokemonApi { - static const String baseUrl = 'pokeapi.co'; - static const String pokemonUrl = 'api/v2/pokemon'; + static const String baseUrl = 'tyradex.vercel.app'; + static const String pokemonUrl = 'api/v1/pokemon'; static Future 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 @@ -20,15 +20,24 @@ class PokemonApi { } // 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; + // Récupération du nom en français + String name = json['name']['fr'] ?? json['name']['en'] ?? 'unknown'; + + // Récupération des types (en français dans l'API Tyradex) + List types = json['types'] ?? []; + PokemonType type1 = types.isNotEmpty + ? frenchTypeToEnum(types[0]['name']) + : PokemonType.unknown; + PokemonType? type2 = types.length > 1 + ? frenchTypeToEnum(types[1]['name']) + : null; + // On crée un objet Pokemon à partir du fichier JSON return Pokemon( name: name, id: id, - type1: PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type1'), - type2: type2 != null ? PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type2') : null, + type1: type1, + type2: type2, ); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c507685..662ee93 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'pages/home_page.dart'; +import 'pages/game_page.dart'; import 'pages/pokemon_list.dart'; import 'pages/pokemon_detail.dart'; @@ -17,13 +19,14 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - debugShowCheckedModeBanner: false, // Permet de masquer la bannière "Debug" - // home a été enlevé pour être remplacé par la route "/" + debugShowCheckedModeBanner: + false, // Permet de masquer la bannière "Debug" routes: { - '/': (context) => const PokemonListPage(), // La route "/" est la page d'accueil - '/pokemon-detail':(context) => const PokemonDetailPage(), - } + '/': (context) => const HomePage(), // Menu principal + '/game': (context) => const GamePage(), // Page de jeu + '/pokedex': (context) => const PokemonListPage(), // Liste des Pokémon + '/pokemon-detail': (context) => const PokemonDetailPage(), + }, ); } } - diff --git a/lib/models/pokemon.dart b/lib/models/pokemon.dart index 03c1a76..e990c8e 100644 --- a/lib/models/pokemon.dart +++ b/lib/models/pokemon.dart @@ -12,8 +12,8 @@ class Pokemon { 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 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 { diff --git a/lib/pages/game_page.dart b/lib/pages/game_page.dart new file mode 100644 index 0000000..8d0ab51 --- /dev/null +++ b/lib/pages/game_page.dart @@ -0,0 +1,519 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import '../models/pokemon.dart'; + +/// GamePage - Main quiz game where users guess Pokémon from silhouettes +class GamePage extends StatefulWidget { + const GamePage({super.key}); + + @override + State createState() => _GamePageState(); +} + +class _GamePageState extends State { + // Game state + int score = 0; + int lives = 3; + Pokemon? currentPokemon; + bool isShiny = false; + bool isRevealed = false; + bool showHint = false; + bool isLoading = true; + + final TextEditingController _controller = TextEditingController(); + final Random _random = Random(); + + @override + void initState() { + super.initState(); + _loadNewPokemon(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + /// Load a new random Pokémon (1-151) + Future _loadNewPokemon() async { + setState(() { + isLoading = true; + isRevealed = false; + showHint = false; + _controller.clear(); + }); + + // Random ID between 1 and 151 + final int randomId = _random.nextInt(151) + 1; + + // Shiny chance: 1/20 (5%) + final bool shiny = _random.nextInt(20) == 0; + + // Fetch Pokémon using existing model (uses cache/API layer) + final Pokemon? pokemon = await Pokemon.fromID(randomId); + + if (mounted) { + setState(() { + currentPokemon = pokemon; + isShiny = shiny; + isLoading = false; + }); + } + } + + /// Validate user's guess + void _validateGuess() { + // Hide keyboard + FocusScope.of(context).unfocus(); + + if (currentPokemon == null) return; + + final String userInput = _controller.text.trim().toLowerCase(); + final String correctName = currentPokemon!.name.toLowerCase(); + + if (userInput == correctName) { + _handleCorrectGuess(); + } else { + _handleWrongGuess(); + } + } + + /// Handle correct guess + void _handleCorrectGuess() { + final int points = isShiny ? 20 : 10; + + setState(() { + isRevealed = true; + score += points; + }); + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + isShiny ? '✨ SHINY! +$points points!' : '✅ Correct! +$points points!', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + backgroundColor: isShiny ? Colors.amber : Colors.green, + duration: const Duration(seconds: 2), + ), + ); + + // Wait 2 seconds, then load new Pokémon + Timer(const Duration(seconds: 2), () { + if (mounted) { + _loadNewPokemon(); + } + }); + } + + /// Handle wrong guess + void _handleWrongGuess() { + setState(() { + lives--; + }); + + if (lives <= 0) { + _showGameOverDialog(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '❌ Incorrect! Il reste $lives vie${lives > 1 ? 's' : ''}', + style: const TextStyle(fontSize: 16), + ), + backgroundColor: Colors.redAccent, + duration: const Duration(seconds: 1), + ), + ); + } + } + + /// Show game over dialog + void _showGameOverDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text( + 'GAME OVER', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Le Pokémon était:', + style: TextStyle(color: Colors.white70, fontSize: 16), + ), + const SizedBox(height: 8), + Text( + currentPokemon?.formatedName ?? '???', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + const Text( + 'Score final', + style: TextStyle(color: Colors.white70, fontSize: 16), + ), + Text( + '$score', + style: const TextStyle( + color: Color(0xFFE94560), + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + actions: [ + Center( + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + _restartGame(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE94560), + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + ), + child: const Text( + 'REJOUER', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } + + /// Restart the game + void _restartGame() { + setState(() { + score = 0; + lives = 3; + }); + _loadNewPokemon(); + } + + /// Use hint (costs 5 points) + void _useHint() { + if (score >= 5 && !showHint) { + setState(() { + score -= 5; + showHint = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF1A1A2E), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'POKÉGUESS', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Score and Lives row + _buildScoreAndLives(), + const SizedBox(height: 20), + + // Pokémon silhouette + Expanded( + child: _buildPokemonDisplay(), + ), + + // Hint display + if (showHint && currentPokemon != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: currentPokemon!.type1Color.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: currentPokemon!.type1Color), + ), + child: Text( + 'Type: ${currentPokemon!.type1Formated}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + // Input and buttons + _buildInputSection(), + ], + ), + ), + ), + ); + } + + /// Build score and lives display + Widget _buildScoreAndLives() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Score + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon(Icons.star, color: Colors.amber, size: 24), + const SizedBox(width: 8), + Text( + '$score', + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + // Lives (hearts) + Row( + children: List.generate(3, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon( + index < lives ? Icons.favorite : Icons.favorite_border, + color: const Color(0xFFE94560), + size: 32, + ), + ); + }), + ), + ], + ); + } + + /// Build Pokémon silhouette or revealed image + Widget _buildPokemonDisplay() { + if (isLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFFE94560), + ), + ); + } + + if (currentPokemon == null) { + return const Center( + child: Text( + 'Erreur de chargement', + style: TextStyle(color: Colors.white), + ), + ); + } + + final String imageUrl = + isShiny ? currentPokemon!.shinyImageUrl : currentPokemon!.imageUrl; + + Widget image = Image.network( + imageUrl, + width: 250, + height: 250, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const SizedBox( + width: 250, + height: 250, + child: Center( + child: CircularProgressIndicator(color: Color(0xFFE94560)), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return const SizedBox( + width: 250, + height: 250, + child: Center( + child: Icon(Icons.error, color: Colors.red, size: 50), + ), + ); + }, + ); + + // Apply silhouette filter if not revealed + if (!isRevealed) { + image = ColorFiltered( + colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), + child: image, + ); + } + + // Add shiny sparkle effect if revealed and shiny + return Center( + child: Stack( + alignment: Alignment.center, + children: [ + // Glow effect when revealed + if (isRevealed) + Container( + width: 280, + height: 280, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: isShiny + ? Colors.amber.withValues(alpha: 0.4) + : Colors.blueAccent.withValues(alpha: 0.3), + blurRadius: 40, + spreadRadius: 10, + ), + ], + ), + ), + image, + // Shiny indicator + if (isShiny && isRevealed) + const Positioned( + top: 0, + right: 50, + child: Text( + '✨', + style: TextStyle(fontSize: 32), + ), + ), + ], + ), + ); + } + + /// Build input section with text field and buttons + Widget _buildInputSection() { + return Column( + children: [ + // Text input + Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(15), + ), + child: TextField( + controller: _controller, + enabled: !isRevealed, + style: const TextStyle(color: Colors.white, fontSize: 18), + textAlign: TextAlign.center, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + hintText: 'Nom du Pokémon...', + hintStyle: TextStyle(color: Colors.white38), + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + onSubmitted: (_) => _validateGuess(), + ), + ), + const SizedBox(height: 16), + + // Buttons row + Row( + children: [ + // Hint button + Expanded( + child: ElevatedButton.icon( + onPressed: + score >= 5 && !showHint && !isRevealed ? _useHint : null, + icon: const Icon(Icons.lightbulb_outline, size: 20), + label: const Text('Indice (5 pts)'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.amber.withValues(alpha: 0.8), + foregroundColor: Colors.black87, + disabledBackgroundColor: Colors.grey.withValues(alpha: 0.3), + disabledForegroundColor: Colors.white38, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 10), + + // Validate button + Expanded( + flex: 1, + child: ElevatedButton( + onPressed: !isRevealed ? _validateGuess : null, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE94560), + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey.withValues(alpha: 0.3), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'VALIDER', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..ac0f0fc --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + +/// HomePage - Main menu for Pokéguess +/// Contains PLAY and POKEDEX navigation buttons +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF1A1A2E), + Color(0xFF16213E), + Color(0xFF0F3460), + ], + ), + ), + child: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo / Title + const Text( + 'POKÉGUESS', + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 4, + shadows: [ + Shadow( + blurRadius: 20, + color: Colors.blueAccent, + offset: Offset(0, 0), + ), + ], + ), + ), + const SizedBox(height: 8), + const Text( + 'Devine le Pokémon !', + style: TextStyle( + fontSize: 18, + color: Colors.white70, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(height: 80), + + // PLAY Button + _MenuButton( + label: 'JOUER', + icon: Icons.play_arrow_rounded, + color: const Color(0xFFE94560), + onPressed: () => Navigator.pushNamed(context, '/game'), + ), + const SizedBox(height: 24), + + // POKEDEX Button + _MenuButton( + label: 'POKÉDEX', + icon: Icons.catching_pokemon, + color: const Color(0xFF0F3460), + borderColor: Colors.white38, + onPressed: () => Navigator.pushNamed(context, '/pokedex'), + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Reusable menu button with icon and gradient effect +class _MenuButton extends StatelessWidget { + final String label; + final IconData icon; + final Color color; + final Color? borderColor; + final VoidCallback onPressed; + + const _MenuButton({ + required this.label, + required this.icon, + required this.color, + required this.onPressed, + this.borderColor, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 220, + height: 60, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + elevation: 8, + shadowColor: color.withValues(alpha: 0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + side: borderColor != null + ? BorderSide(color: borderColor!, width: 2) + : BorderSide.none, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 28), + const SizedBox(width: 12), + Text( + label, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/pokemon_list.dart b/lib/pages/pokemon_list.dart index a29e569..5e0a016 100644 --- a/lib/pages/pokemon_list.dart +++ b/lib/pages/pokemon_list.dart @@ -1,7 +1,8 @@ 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 +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). @@ -13,7 +14,6 @@ class PokemonListPage extends StatefulWidget { } class _PokemonListPageState extends State { - 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 @@ -51,19 +51,21 @@ class _PokemonListPageState extends State { 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, - ) - ), + 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, + )), ); } -} \ No newline at end of file +} diff --git a/lib/utils/pokemon_type.dart b/lib/utils/pokemon_type.dart index 7a27cd2..120bb32 100644 --- a/lib/utils/pokemon_type.dart +++ b/lib/utils/pokemon_type.dart @@ -1,6 +1,31 @@ import 'package:flutter/material.dart'; import '../models/pokemon.dart'; +// Convertit un nom de type français (de l'API Tyradex) en PokemonType +PokemonType frenchTypeToEnum(String frenchType) { + const Map frenchToEnglish = { + 'Normal': PokemonType.normal, + 'Combat': PokemonType.fighting, + 'Vol': PokemonType.flying, + 'Poison': PokemonType.poison, + 'Sol': PokemonType.ground, + 'Roche': PokemonType.rock, + 'Insecte': PokemonType.bug, + 'Spectre': PokemonType.ghost, + 'Acier': PokemonType.steel, + 'Feu': PokemonType.fire, + 'Eau': PokemonType.water, + 'Plante': PokemonType.grass, + 'Électrik': PokemonType.electric, + 'Psy': PokemonType.psychic, + 'Glace': PokemonType.ice, + 'Dragon': PokemonType.dragon, + 'Ténèbres': PokemonType.dark, + 'Fée': PokemonType.fairy, + }; + return frenchToEnglish[frenchType] ?? PokemonType.unknown; +} + // Permet de mapper un type de Pokémon avec une couleur Color typeToColor(PokemonType type) { Map typeToColor = { -- 2.49.1