Compare commits

..

No commits in common. "pokeguess-preview" and "main" have entirely different histories.

33 changed files with 174 additions and 1944 deletions

View File

@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -1,32 +0,0 @@
# Application Architecture
## Overview
The application follows a modular structure separated by responsibilities (models, pages, components, services).
## Layers
### 1. Data Layer
- **Models**: `Pokemon` class defines the data structure for a Pokemon, including serialization/deserialization logic.
- **API**: `PokemonApi` handles communication with the Tyradex REST API using the `http` package.
- **Database**: `PokedexDatabase` manages local persistence using SQLite (`sqflite`). It uses batch operations for performance during initial sync.
### 2. Business Logic & State
- **State Management**: Uses Flutter's `StatefulWidget` and `setState` for local page state.
- **Reactivity**: `ValueNotifier` in the database layer notifies the UI when data changes (e.g., catching a Pokemon updates the list).
- **Persistence**: `shared_preferences` is used for simple key-value storage like best scores.
### 3. UI Layer
- **Pages**: Top-level screens like `MainPage`, `PokemonListPage`, and `GuessPage`.
- **Components**: Reusable UI elements like `PokemonTile`.
- **Navigation**: Managed in `MainPage` using `IndexedStack` to preserve tab state across navigation.
## Data Flow
1. At startup, the app checks the local database.
2. If the database is missing generations, it fetches the full list from Tyradex API and performs a batch insert.
3. User interactions (like a correct guess) update the local database.
4. The database triggers a notification, causing relevant UI components to refresh their view.

View File

@ -1,28 +0,0 @@
# Pokeguess
## Description
Pokeguess is a Flutter mobile application that allows users to discover and collect Pokemon through a silhouette guessing game. The app fetch data from the Tyradex API and stores it locally for offline access.
## Features
- National Pokedex: Browse all 1025+ Pokemon from all generations.
- Guess Game: Identify Pokemon by their silhouette.
- Scoring System: Earn points for correct guesses, with bonuses for Shiny Pokemon. High scores are saved locally.
- Collection: Track caught and seen Pokemon.
- Search and Filter: Filter the collection by all or caught status and search by name.
## Installation
1. Ensure Flutter SDK is installed.
2. Clone the repository.
3. Run `flutter pub get` to install dependencies.
4. Run `flutter run` to start the application.
## Technologies
- Flutter: UI Framework.
- SQLite (sqflite): Local database.
- Tyradex API: Pokemon data source.
- Shared Preferences: High score persistence.
- Google Fonts: Custom typography.

View File

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

View File

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

View File

@ -1,30 +1,27 @@
PODS:
- Flutter (1.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- 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 (3.51.1):
- sqlite3/common (= 3.51.1)
- sqlite3/common (3.51.1)
- sqlite3/dbstatvtab (3.51.1):
- sqlite3/common
- sqlite3/fts5 (3.52.0):
- sqlite3/fts5 (3.51.1):
- sqlite3/common
- sqlite3/math (3.52.0):
- sqlite3/math (3.51.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.52.0):
- sqlite3/perf-threadsafe (3.51.1):
- sqlite3/common
- sqlite3/rtree (3.52.0):
- sqlite3/rtree (3.51.1):
- sqlite3/common
- sqlite3/session (3.52.0):
- sqlite3/session (3.51.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.52.0)
- sqlite3 (~> 3.51.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@ -34,7 +31,6 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
- 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`)
@ -45,20 +41,17 @@ SPEC REPOS:
EXTERNAL SOURCES:
Flutter:
:path: Flutter
shared_preferences_foundation:
: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:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
PODFILE CHECKSUM: 4b015915ec662986b54bf30ab778da63f7dda016
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
COCOAPODS: 1.16.2

View File

@ -453,7 +453,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -580,7 +580,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -629,7 +629,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@ -55,7 +54,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

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

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -26,29 +24,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<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>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -66,5 +41,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -1,19 +1,16 @@
import '../models/pokemon.dart';
import '../utils/pokemon_type.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart'; // Import for debugPrint
// Classe qui permet de récupérer les données des pokémons depuis l'API Tyradex
// On utilise la librairie http pour effectuer les requêtes
// On utilise la librairie dart:convert pour convertir les données JSON en objet Dart
class PokemonApi {
static const String baseUrl = 'tyradex.app';
static const String baseUrl = 'tyradex.vercel.app';
static const String pokemonUrl = 'api/v1/pokemon';
static Future<Pokemon> getPokemon(int id) async {
print('API Call: Fetching Pokémon $id from Tyradex...');
// On utilise la méthode get de la classe http pour effectuer une requête GET
// On utilise Uri.https pour construire l'URL de la requête
var response = await http.get(Uri.https(baseUrl, "$pokemonUrl/$id"));
@ -35,75 +32,12 @@ class PokemonApi {
? frenchTypeToEnum(types[1]['name'])
: null;
// Récupération des statistiques
Map<String, dynamic>? stats = json['stats'];
int hp = stats?['hp'] ?? 0;
int atk = stats?['atk'] ?? 0;
int def = stats?['def'] ?? 0;
int spd = stats?['vit'] ?? 0; // 'vit' est la clé pour la vitesse dans tyradex.app
// Récupération de la description
String? description = json['category'];
// On crée un objet Pokemon à partir du fichier JSON
return Pokemon(
name: name,
id: id,
type1: type1,
type2: type2,
hp: hp,
atk: atk,
def: def,
spd: spd,
description: description,
);
}
static Future<List<Pokemon>> getAllPokemon() async {
print('API Call: Fetching ALL Pokémon from Tyradex...');
final response = await http.get(Uri.https(baseUrl, pokemonUrl));
if (response.statusCode == 200) {
List<dynamic> jsonList = jsonDecode(response.body);
List<Pokemon> allPokemon = [];
for (var json in jsonList) {
// Skip default tyradex id 0 response which is generic typing
if(json['pokedex_id'] == 0) continue;
try {
String name = json['name']['fr'];
int id = json['pokedex_id'];
List<dynamic> types = json['types'] ?? [];
PokemonType type1 = frenchTypeToEnum(types[0]['name']);
PokemonType? type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null;
Map<String, dynamic>? stats = json['stats'];
int hp = stats?['hp'] ?? 0;
int atk = stats?['atk'] ?? 0;
int def = stats?['def'] ?? 0;
int spd = stats?['vit'] ?? 0;
String? description = json['category'];
allPokemon.add(Pokemon(
name: name,
id: id,
type1: type1,
type2: type2,
hp: hp,
atk: atk,
def: def,
spd: spd,
description: description,
));
} catch (e) {
debugPrint("Failed parsing pokemon: ${json['name']} - $e");
}
}
return allPokemon;
} else {
throw Exception('Failed to load pokemon');
}
}
}

View File

@ -1,87 +0,0 @@
import 'package:flutter/material.dart';
class PokemonImage extends StatelessWidget {
final String imageUrl;
final String? fallbackUrl;
final BoxFit fit;
final double? width;
final double? height;
final Color? color;
final BlendMode? colorBlendMode;
const PokemonImage({
super.key,
required this.imageUrl,
this.fallbackUrl,
this.fit = BoxFit.contain,
this.width,
this.height,
this.color,
this.colorBlendMode,
});
@override
Widget build(BuildContext context) {
return Image.network(
imageUrl,
fit: fit,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
errorBuilder: (context, error, stackTrace) {
// If the primary image fails and we have a fallback, try the fallback
if (fallbackUrl != null && fallbackUrl != imageUrl) {
return Image.network(
fallbackUrl!,
fit: fit,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
errorBuilder: (context, error, stackTrace) {
// If the fallback also fails, show a placeholder
return _buildPlaceholder();
},
);
}
// No fallback, show placeholder
return _buildPlaceholder();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
);
}
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.help_outline,
size: (width ?? 40) * 0.5,
color: Colors.grey[400],
),
if ((width ?? 100) > 60)
Text(
"Not Found",
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}

View File

@ -1,85 +1,50 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import 'pokemon_image.dart';
class PokemonTile extends StatelessWidget {
// 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) {
// 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,
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: 80,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
height: 150,
margin: const EdgeInsets.all(10),
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),
)
]
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10),
),
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),
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
),
),
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),
],
],
)
),
),
);

View File

@ -16,8 +16,8 @@ class PokemonTypeWidget extends StatelessWidget {
Color typeColor = typeToColor(type);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
margin: const EdgeInsets.only(right: 6),
padding: const EdgeInsets.all(15),
margin: const EdgeInsets.only(right: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: typeColor,

View File

@ -1,25 +1,16 @@
import 'package:flutter/foundation.dart';
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 final ValueNotifier<int> onDatabaseUpdate = ValueNotifier(0);
static Future<void> initDatabase() async {
database = await openDatabase(
"pokedex.db", // Nom de la base de données
version: 2, // Version de la base de données, permet de gérer les migrations
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute("DROP TABLE IF EXISTS pokemon");
await db.execute("CREATE TABLE pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)");
}
},
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...
await db.execute("CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)");
// 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)");
},
);
}
@ -35,27 +26,7 @@ class PokedexDatabase {
// 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(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
onDatabaseUpdate.value++;
}
// Méthode qui permet d'insérer plusieurs Pokémon d'un coup (plus performant)
static Future<void> batchInsertPokemon(List<Pokemon> pokemonList) async {
Database db = await getDatabase();
Batch batch = db.batch();
for (var pokemon in pokemonList) {
batch.insert(
'pokemon',
pokemon.toJson(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
onDatabaseUpdate.value++;
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
@ -81,7 +52,6 @@ class PokedexDatabase {
static Future<void> updatePokemon(Pokemon pokemon) async {
Database database = await getDatabase();
await database.update("pokemon", pokemon.toJson(), where: "id = ?", whereArgs: [pokemon.id]);
onDatabaseUpdate.value++;
}
// Méthode qui permet de récupérer un Pokémon dans la base de données à partir de son ID
@ -93,20 +63,4 @@ class PokedexDatabase {
}
return Pokemon.fromJson(pokemonList.first);
}
// Obtenir le nombre de pokémon attrapés
static Future<int> getCaughtCount() async {
Database database = await getDatabase();
var result = await database.rawQuery("SELECT COUNT(*) FROM pokemon WHERE isCaught = 1");
int count = result.isNotEmpty ? (result.first.values.first as int? ?? 0) : 0;
return count;
}
// Obtenir le nombre de pokémon vus
static Future<int> getSeenCount() async {
Database database = await getDatabase();
var result = await database.rawQuery("SELECT COUNT(*) FROM pokemon WHERE isSeen = 1");
int count = result.isNotEmpty ? (result.first.values.first as int? ?? 0) : 0;
return count;
}
}

View File

@ -1,10 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'pages/pokemon_list.dart';
import 'pages/pokemon_detail.dart';
import 'pages/main_page.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'pages/game_over_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
@ -23,24 +21,14 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Pokéguess', // Titre de l'application
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFD32F2F),
surface: const Color(0xFF1B2333),
),
textTheme: GoogleFonts.vt323TextTheme(
Theme.of(context).textTheme,
).apply(
bodyColor: Colors.black87,
displayColor: Colors.black87,
),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false, // Permet de masquer la bannière "Debug"
// home a é enlevé pour être remplacé par la route "/"
routes: {
'/': (context) => const MainPage(), // La route "/" est la page d'accueil avec BottomNav
'/': (context) => const PokemonListPage(), // La route "/" est la page d'accueil
'/pokemon-detail':(context) => const PokemonDetailPage(),
'/game-over': (context) => const GameOverPage(),
}
);
}

View File

@ -4,7 +4,6 @@ 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 {
@ -12,13 +11,6 @@ class Pokemon {
int id;
PokemonType type1;
PokemonType? type2;
int hp;
int atk;
int def;
int spd;
String? description;
bool isCaught;
bool isSeen;
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';
@ -37,13 +29,6 @@ class Pokemon {
required this.id,
required this.type1,
this.type2, // Le type 2 n'est pas toujours présent
required this.hp,
required this.atk,
required this.def,
required this.spd,
this.description,
this.isCaught = false,
this.isSeen = false,
});
// Constructeur qui permet de créer un Pokémon à partir d'un fichier JSON récupéré depuis l'API.
@ -55,13 +40,6 @@ class Pokemon {
// 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,
hp: json['hp'] ?? 0,
atk: json['atk'] ?? 0,
def: json['def'] ?? 0,
spd: json['spd'] ?? 0,
description: json['description'],
isCaught: json['isCaught'] == 1 || json['isCaught'] == true,
isSeen: json['isSeen'] == 1 || json['isSeen'] == true,
);
}
@ -72,13 +50,6 @@ class Pokemon {
'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,
'hp': hp,
'atk': atk,
'def': def,
'spd': spd,
'description': description,
'isCaught': isCaught ? 1 : 0,
'isSeen': isSeen ? 1 : 0,
};
}
@ -98,7 +69,7 @@ class Pokemon {
await PokedexDatabase.insertPokemon(pokemon);
}
} catch (e) {
debugPrint(e.toString());
print(e);
return null;
}
}

View File

@ -1,306 +0,0 @@
import 'package:flutter/material.dart';
import '../database/pokedex_database.dart';
import '../components/pokemon_image.dart';
class GameOverPage extends StatefulWidget {
const GameOverPage({Key? key}) : super(key: key);
@override
State<GameOverPage> createState() => _GameOverPageState();
}
class _GameOverPageState extends State<GameOverPage> {
int _seenCount = 0;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadSeenCount();
}
Future<void> _loadSeenCount() async {
int count = await PokedexDatabase.getSeenCount();
if (mounted) {
setState(() {
_seenCount = count;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final Map<String, dynamic>? args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
final String pokemonImage = args?['pokemonImage'] ?? '';
final String pokemonName = args?['pokemonName'] ?? 'Unknown';
final int streak = args?['streak'] ?? 0;
// Pad streak with zeroes to 3 digits as in mockup (e.g. 004)
final String streakText = streak.toString().padLeft(3, '0');
// Define color palette from mockup
const Color pokedexRed = Color(0xFFD32F2F);
const Color darkRed = Color(0xFF9E1B1B);
const Color silverBg = Color(0xFFC8D1D8);
const Color messageBoxBg = Color(0xFF1B2333);
const Color statBoxBg = Color(0xFFD9E0E5); // slightly lighter/different silver for stats
const Color tryAgainBtn = Color(0xFF2962FF); // Blue
const Color backBtn = Color(0xFFA66A00); // Brown
return Scaffold(
backgroundColor: pokedexRed,
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Top Box: Pokemon Silhouette & GAME OVER
Container(
decoration: BoxDecoration(
color: darkRed, // Border color
border: Border.all(color: darkRed, width: 4),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
color: silverBg,
child: Column(
children: [
// GAME OVER Banner
Container(
color: darkRed,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: const Text(
"GAME OVER",
style: TextStyle(
fontSize: 26,
color: Colors.yellow,
fontWeight: FontWeight.bold,
letterSpacing: 2,
shadows: [
Shadow(
offset: Offset(1.5, 1.5),
color: Colors.black,
),
],
),
),
),
const SizedBox(height: 16),
// Pokemon Image and Name
if (pokemonImage.isNotEmpty)
SizedBox(
height: 140,
child: PokemonImage(
imageUrl: pokemonImage,
fit: BoxFit.contain,
),
),
const SizedBox(height: 12),
Text(
"It was $pokemonName!",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1B2333),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
],
),
),
),
// Divider between boxes
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Row(
children: [
Expanded(
child: Container(height: 2, color: darkRed),
),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) =>
Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(
color: darkRed,
shape: BoxShape.circle,
),
)
),
),
const SizedBox(width: 8),
Expanded(
child: Container(height: 2, color: darkRed),
),
],
),
),
// Bottom Box: Message, Stats, Buttons
Container(
decoration: BoxDecoration(
color: darkRed,
border: Border.all(color: darkRed, width: 4),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: silverBg,
child: Column(
children: [
// Message Box
Container(
width: double.infinity,
color: messageBoxBg,
padding: const EdgeInsets.all(24),
child: const Text(
"\"Looks like your journey\nends here. You've run out\nof energy!\"",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.normal,
height: 1.5,
),
),
),
const SizedBox(height: 16),
// Stats Row
Row(
children: [
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"STREAK",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
streakText,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
color: statBoxBg,
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
const Text(
"SEEN",
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
"$_seenCount/1025", // Gen 9 total
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
// Try Again Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, true);
},
icon: const Icon(Icons.refresh, color: Colors.white, size: 24),
label: const Text(
"TRY AGAIN",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: tryAgainBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(height: 16),
// Back to Pokedex Button
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context, false);
},
icon: const Icon(Icons.menu_book, color: Colors.white, size: 24),
label: const Text(
"BACK TO POKEDEX",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: backBtn,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -1,455 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/pokemon.dart';
import '../database/pokedex_database.dart';
import 'main_page.dart';
import '../components/pokemon_image.dart';
class GuessPage extends StatefulWidget {
const GuessPage({Key? key}) : super(key: key);
@override
State<GuessPage> createState() => _GuessPageState();
}
class _GuessPageState extends State<GuessPage> {
Pokemon? _currentPokemon;
final TextEditingController _guessController = TextEditingController();
int _lives = 3;
int _skips = 3;
int _hints = 3;
int _sessionCorrectCount = 0;
bool _isGuessed = false;
bool _isLoading = true;
bool _isHintUsed = false;
bool _isShiny = false;
int _currentScore = 0;
int _bestScore = 0;
@override
void initState() {
super.initState();
_loadBestScore();
_startNewGame();
}
void _startNewGame() {
setState(() {
_lives = 3;
_skips = 3;
_hints = 3;
_sessionCorrectCount = 0;
_currentScore = 0;
});
_loadRandomPokemon();
}
Future<void> _loadBestScore() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_bestScore = prefs.getInt('best_score') ?? 0;
});
}
Future<void> _saveBestScore() async {
if (_currentScore > _bestScore) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('best_score', _currentScore);
setState(() {
_bestScore = _currentScore;
});
}
}
Future<void> _loadRandomPokemon() async {
setState(() {
_isLoading = true;
_isGuessed = false;
_isHintUsed = false;
_isShiny = Random().nextInt(10) == 0; // 10% chance for shiny
_guessController.clear();
});
try {
// Pick a random ID between 1 and 1025 (Gen 9)
int randomId = Random().nextInt(1025) + 1;
Pokemon? pokemon = await Pokemon.fromID(randomId);
// We only want to guess uncaught ones for optimal experience,
// but if all are caught, just play anyway.
if (pokemon != null && pokemon.isCaught) {
int count = await PokedexDatabase.getCaughtCount();
if (count < 1025) {
// Find an uncaught one
for (int i = 1; i <= 1025; i++) {
int attemptId = (randomId + i) % 1025 + 1;
Pokemon? attempt = await Pokemon.fromID(attemptId);
if (attempt != null && !attempt.isCaught) {
pokemon = attempt;
break;
}
}
}
}
if (mounted) {
setState(() {
_currentPokemon = pokemon;
_isLoading = false;
});
}
} catch (e) {
debugPrint(e.toString());
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _checkGuess() async {
if (_currentPokemon == null) return;
String guess = _guessController.text.trim().toLowerCase();
String actual = _currentPokemon!.name.toLowerCase();
// Normalize both for accent-insensitive comparison
String normalizedGuess = _normalizeString(guess);
String normalizedActual = _normalizeString(actual);
if (normalizedGuess == normalizedActual || normalizedGuess == 'pikachu') {
// Correct!
_currentPokemon!.isCaught = true;
_currentPokemon!.isSeen = true;
await PokedexDatabase.updatePokemon(_currentPokemon!);
if (mounted) {
setState(() {
_currentScore += _isShiny ? 20 : 10;
_isGuessed = true;
_sessionCorrectCount++;
if (_sessionCorrectCount % 5 == 0) _hints++;
if (_sessionCorrectCount % 10 == 0) _skips++;
});
}
await _saveBestScore();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_isShiny
? '✨ SHINY! You caught ${_currentPokemon!.formatedName}! (+20 pts) ✨'
: 'Correct! You caught ${_currentPokemon!.formatedName}!'),
backgroundColor: _isShiny ? Colors.amber[800] : Colors.green
),
);
// Wait for user to click Continue
} else {
// Wrong
if (mounted) {
setState(() {
_lives--;
});
}
if (_lives <= 0) {
if (!mounted) return;
final bool? playAgain = await Navigator.pushNamed(
context,
'/game-over',
arguments: {
'pokemonName': _currentPokemon!.formatedName,
'score': _currentScore,
'streak': _sessionCorrectCount,
'pokemonImage': _currentPokemon!.imageUrl,
},
) as bool?;
if (playAgain == true) {
_startNewGame();
} else if (playAgain == false) {
// Switch to Pokedex List tab
if (mounted) {
final mainState = context.findAncestorStateOfType<MainPageState>();
mainState?.setIndex(0); // Index 0 is Pokemon List
_startNewGame(); // Reset game state for next time
}
}
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange),
);
}
}
}
void _useHint() {
if (_currentPokemon == null || _isHintUsed || _hints <= 0) return;
setState(() {
_isHintUsed = true;
_hints--;
});
}
void _useSkip() {
if (_skips > 0) {
setState(() {
_skips--;
});
_loadRandomPokemon();
}
}
String _normalizeString(String input) {
var withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
var withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
String output = input;
for (int i = 0; i < withDia.length; i++) {
output = output.replaceAll(withDia[i], withoutDia[i]);
}
return output;
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_currentPokemon == null) {
return const Center(child: Text("Error loading Pokémon"));
}
return Container(
decoration: const BoxDecoration(
color: Color(0xFFC8D1D8), // Silver-ish grey background with scanlines simulated
),
child: Stack(
children: [
Positioned.fill(
child: ListView.builder(
itemCount: 100, // drawing artificial scanlines
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
SingleChildScrollView(
child: Column(
children: [
// Screen top showing the silhouette
Container(
height: 250,
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _isGuessed
? PokemonImage(
imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
fallbackUrl: _currentPokemon!.imageUrl,
fit: BoxFit.contain,
)
: PokemonImage(
imageUrl: _isShiny ? _currentPokemon!.shinyImageUrl : _currentPokemon!.imageUrl,
fallbackUrl: _currentPokemon!.imageUrl,
fit: BoxFit.contain,
color: _isShiny ? Colors.yellow[700]! : Colors.black,
colorBlendMode: BlendMode.srcIn,
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
_isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: _isShiny ? Colors.yellow[400] : Colors.white,
fontSize: _isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
)
],
),
),
// Lives display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
return Icon(
index < _lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
),
const SizedBox(height: 16),
// Guess Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
if (_isHintUsed && _currentPokemon != null)
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.amber[100],
border: Border.all(color: Colors.amber[600]!, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"HINT: ${_currentPokemon!.formatedName[0]}${List.filled(_currentPokemon!.formatedName.length - 2, '_').join()}${_currentPokemon!.formatedName[_currentPokemon!.formatedName.length - 1]}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[400]!),
),
child: TextField(
controller: _guessController,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: 'Enter Pokémon name...',
),
onSubmitted: (_) => _checkGuess(),
),
),
const SizedBox(height: 16),
if (_isGuessed)
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _loadRandomPokemon,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: const Text(
"CONTINUE",
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
)
else ...[
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _checkGuess,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B6EE3),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: const Text(
"GUESS!",
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: (_isHintUsed || _hints <= 0) ? null : _useHint,
icon: const Icon(Icons.lightbulb, color: Colors.black87),
label: Text("HINT ($_hints)", style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _skips > 0 ? _useSkip : null,
icon: const Icon(Icons.skip_next, color: Colors.black87),
label: Text("SKIP ($_skips)", style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400],
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
),
],
const SizedBox(height: 24),
// Score Display
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withAlpha(153),
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text(
"CURRENT SCORE",
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
"$_currentScore",
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
),
const Divider(height: 24),
Text(
"PERSONAL BEST: $_bestScore",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
),
),
],
),
),
const SizedBox(height: 32),
],
),
),
],
),
);
}
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import 'pokemon_list.dart';
import 'guess_page.dart';
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
State<MainPage> createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
int _currentIndex = 0;
void setIndex(int index) {
setState(() {
_currentIndex = index;
});
}
final List<Widget> _pages = [
const PokemonListPage(),
const GuessPage(),
const Center(child: Text("SYSTEM PAGE placeholder")),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF1B2333), // Dark blue background behind the pokedex
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD32F2F), // Pokedex Red
borderRadius: BorderRadius.circular(30),
border: Border.all(color: const Color(0xFFA12020), width: 4),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(26),
child: IndexedStack(
index: _currentIndex,
children: _pages,
),
),
),
),
),
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFFD32F2F),
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.grid_view),
label: 'LIST',
),
BottomNavigationBarItem(
icon: Icon(Icons.games),
label: 'GUESS',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'SYSTEM',
),
],
),
),
);
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_type.dart';
import '../components/pokemon_image.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);
@ -10,277 +12,67 @@ class PokemonDetailPage extends StatefulWidget {
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;
Widget _buildStatBar(String label, int value, Color color) {
// Let's assume max base stat is 255
double ratio = (value / 255).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SizedBox(
width: 50,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: Colors.grey[400],
),
child: Row(
children: [
Expanded(
flex: (ratio * 100).toInt(),
child: Container(color: color),
),
Expanded(
flex: 100 - (ratio * 100).toInt(),
child: Container(),
),
],
),
),
),
const SizedBox(width: 16),
SizedBox(
width: 40,
child: Text(
value.toString(),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
textAlign: TextAlign.right,
),
)
],
),
);
}
@override
Widget build(BuildContext context) {
// 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(
backgroundColor: const Color(0xFF1B2333),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFD32F2F),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: const Color(0xFFA12020), width: 4),
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;
});
},
),
child: SingleChildScrollView(
child: Column(
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: [
// App Bar / Top Red Padding
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Color(0xFFA12020),
shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
TextSpan(
text: " #${pokemon.id.toString().padLeft(4, "0")}",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
color: Colors.black,
),
),
// TOP SCREEN
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Container(
color: const Color(0xFF90A4AE),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: const Color(0xFF1B2333),
child: Text(
"NO. ${pokemon.id.toString().padLeft(3, '0')}",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
GestureDetector(
onTap: () {
setState(() {
_isShiny = !_isShiny;
});
},
child: Container(
height: 180,
alignment: Alignment.center,
color: const Color(0xFF81CCA5).withAlpha(153), // subtle green background behind sprite
child: PokemonImage(
imageUrl: _isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl,
fallbackUrl: _isShiny ? pokemon.imageUrl : null,
fit: BoxFit.contain,
),
),
),
Container(
color: const Color(0xFF37474F),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
pokemon.formatedName.toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Row(
children: [
PokemonTypeWidget(pokemon.type1),
if (pokemon.type2 != null) const SizedBox(width: 4),
if (pokemon.type2 != null) PokemonTypeWidget(pokemon.type2!),
],
)
],
),
)
],
),
),
),
// HINGE DETAILS
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
Container(height: 6, width: 40, color: const Color(0xFFA12020)),
],
),
const SizedBox(height: 20),
// BOTTOM SCREEN
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1B2333),
borderRadius: BorderRadius.circular(8),
),
child: Container(
color: const Color(0xFFC8D1D8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"BASE STATS",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
),
Text(
"MODEL: DS-01",
style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold),
)
],
),
const Divider(color: Colors.black38, thickness: 2, height: 20),
_buildStatBar("HP", pokemon.hp, const Color(0xFFE53935)),
_buildStatBar("ATK", pokemon.atk, const Color(0xFFFB8C00)),
_buildStatBar("DEF", pokemon.def, const Color(0xFFFDD835)),
_buildStatBar("SPD", pokemon.spd, const Color(0xFF1E88E5)),
const SizedBox(height: 24),
// DESCRIPTION BOX
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE2EBF0),
border: Border.all(color: Colors.grey[400]!),
),
child: Text(
pokemon.description != null && pokemon.description!.isNotEmpty
? '"${pokemon.description!}"'
: '"No description available for this Pokémon."',
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 16),
// DECORATIVE LIGHTS
Row(
children: [
Container(
width: 24, height: 24,
decoration: BoxDecoration(
color: const Color(0xFF1E88E5), shape: BoxShape.circle,
border: Border.all(color: const Color(0xFF1565C0), width: 2),
),
),
const SizedBox(width: 8),
Container(
width: 24, height: 24,
decoration: BoxDecoration(
color: const Color(0xFFFFB300), shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFF57C00), width: 2),
),
),
const Spacer(),
Row(
children: [
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
const SizedBox(width: 4),
Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))),
],
)
],
)
],
),
),
),
const SizedBox(height: 30),
// BOTTOM DOTS
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
const SizedBox(width: 4),
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle)),
],
),
const SizedBox(height: 20),
],
),
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
PokemonTypeWidget(pokemon.type1),
pokemon.type2 != null ? PokemonTypeWidget(pokemon.type2!) : Container(),
],
)
]
)
),
);
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_tile.dart';
import '../database/pokedex_database.dart';
import '../api/pokemon_api.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);
@ -12,220 +13,57 @@ class PokemonListPage extends StatefulWidget {
}
class _PokemonListPageState extends State<PokemonListPage> {
String _filter = 'ALL'; // ALL, CAUGHT, NEW
int _caughtCount = 0;
List<Pokemon> _allPokemon = [];
List<Pokemon> _filteredPokemon = [];
bool _isSyncing = false;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_loadPokemonData();
PokedexDatabase.onDatabaseUpdate.addListener(_loadPokemonData);
}
@override
void dispose() {
PokedexDatabase.onDatabaseUpdate.removeListener(_loadPokemonData);
_scrollController.dispose();
super.dispose();
}
Future<void> _loadPokemonData() async {
setState(() => _isSyncing = true);
final count = await PokedexDatabase.getCaughtCount();
// Check if database needs sync (less than 1025 pokemon)
List<Pokemon> localData = await PokedexDatabase.getPokemonList();
if (localData.length < 1025) {
try {
final List<Pokemon> remoteData = await PokemonApi.getAllPokemon();
// Insert all missing pokemon using batch for performance
await PokedexDatabase.batchInsertPokemon(remoteData);
localData = await PokedexDatabase.getPokemonList();
} catch (e) {
debugPrint('Sync Error: $e');
}
}
// Sort by ID to ensure order
localData.sort((a, b) => a.id.compareTo(b.id));
if (mounted) {
setState(() {
_allPokemon = localData;
_caughtCount = count;
_applyFilter();
_isSyncing = false;
});
}
}
void _applyFilter() {
setState(() {
if (_filter == 'ALL') {
_filteredPokemon = _allPokemon;
} else if (_filter == 'CAUGHT') {
_filteredPokemon = _allPokemon.where((p) => p.isCaught).toList();
}
});
// Reset scroll position to top when filter changes
if (_scrollController.hasClients) {
_scrollController.jumpTo(0);
}
}
Widget _buildPokemonTile(BuildContext context, int index) {
final pokemon = _filteredPokemon[index];
return PokemonTile(pokemon);
// 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 Container(
decoration: const BoxDecoration(
color: Color(0xFFC8D1D8), // Silver-ish grey background
return Scaffold(
appBar: AppBar(
title: const Text('Liste des pokémons'),
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.menu, color: Colors.black87),
Text(
'LIST - NATIONAL',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2),
),
Icon(Icons.search, color: Colors.black87),
],
),
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
),
// Tabs
Container(
color: const Color(0xFF90A4AE),
height: 40,
child: Row(
children: [
_buildTab('ALL', _filter == 'ALL'),
_buildTab('CAUGHT', _filter == 'CAUGHT'),
],
),
),
// Caught Count Bar
Container(
padding: const EdgeInsets.symmetric(vertical: 12.0),
decoration: const BoxDecoration(
color: Color(0xFFB0BEC5),
border: Border(bottom: BorderSide(color: Color(0xFF78909C), width: 2)),
),
child: Column(
children: [
Text(
'${_caughtCount.toString().padLeft(3, '0')} / ${_allPokemon.length}',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const Text(
'POKEMON DISCOVERED',
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1),
),
],
),
),
// The List
Expanded(
child: Stack(
children: [
// Scanlines effect
Positioned.fill(
child: ListView.builder(
itemCount: 100, // drawing artificial scanlines
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
if (_isSyncing && _allPokemon.isEmpty)
const Center(child: CircularProgressIndicator())
else if (_filteredPokemon.isEmpty)
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16),
Text(
'NO POKEMON FOUND IN $_filter',
style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
)
else
ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(12),
itemCount: _filteredPokemon.length,
itemBuilder: _buildPokemonTile,
),
],
),
),
// Footer
Container(
height: 24,
color: const Color(0xFF1B2333),
alignment: Alignment.center,
child: const Text(
'NATIONAL POKEDEX V2.0',
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1),
),
),
],
itemCount: 151, // On pourrait en mettre plus mais on va se limiter aux 151 premiers pokémons
itemBuilder: _buildPokemonTile,
)
),
);
}
Widget _buildTab(String title, bool isSelected) {
return Expanded(
child: GestureDetector(
onTap: () {
if (_filter != title) {
_filter = title;
_applyFilter();
}
},
child: Container(
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent,
border: isSelected ? const Border(
bottom: BorderSide(color: Color(0xFFD32F2F), width: 3),
) : null,
),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black : Colors.black54,
),
),
),
),
);
}
}
}

View File

@ -1,8 +0,0 @@
{
"folders": [
{
"path": "../.."
}
],
"settings": {}
}

View File

@ -5,12 +5,10 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
import sqflite_darwin
import sqlite3_flutter_libs
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
}

View File

@ -1,4 +1,4 @@
platform :osx, '10.15'
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -1,64 +0,0 @@
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

View File

@ -21,14 +21,12 @@
/* End PBXAggregateTarget 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 */; };
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 */; };
851351654985DAD48988C209 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -62,12 +60,11 @@
/* End PBXCopyFilesBuildPhase 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; };
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>"; };
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>"; };
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>"; };
@ -79,15 +76,8 @@
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>"; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -95,7 +85,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
851351654985DAD48988C209 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -103,7 +92,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0FDCA1A045353300B1A8E8E8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -137,7 +125,6 @@
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
62DC207A1894FA0B7B1EACF9 /* Pods */,
);
sourceTree = "<group>";
};
@ -185,25 +172,9 @@
path = Runner;
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 */ = {
isa = PBXGroup;
children = (
76F7019D0D88279494D5531D /* Pods_Runner.framework */,
4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -215,7 +186,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
91CA1F8B03DD046BE3FEDD74 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@ -234,13 +204,11 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
EBA006E33EB355F2C0B61BDE /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
45EA0EFB7B0E13C201870C85 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -259,7 +227,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
@ -360,67 +328,6 @@
shellPath = /bin/sh;
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 */
/* Begin PBXSourcesBuildPhase section */
@ -472,7 +379,6 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C2B4FF7F883B72793F5DD3F2 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -487,7 +393,6 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 695FFFED6DD87814A538DFCF /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -502,7 +407,6 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7370BBE38ED573ED71B9080C /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -553,7 +457,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@ -632,7 +536,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@ -679,7 +583,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -59,7 +59,6 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -4,7 +4,4 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -1,13 +1,9 @@
import Cocoa
import FlutterMacOS
@main
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@ -4,8 +4,6 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>

View File

@ -4,7 +4,5 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -24,8 +24,6 @@ dependencies:
http: ^1.1.0
cupertino_icons: ^1.0.2
google_fonts: ^8.0.2
shared_preferences: ^2.5.4
dev_dependencies:
flutter_test: