diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee2..620e46e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b7b7533..08957a1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -47,11 +47,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 -PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 34e0dea..edddced 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -453,7 +453,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.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 = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.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 = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada4..e3773d4 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> getPokemon(int id) async { @@ -32,12 +34,74 @@ class PokemonApi { ? frenchTypeToEnum(types[1]['name']) : null; + // Récupération des statistiques + Map? 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> getAllPokemon() async { + final response = await http.get(Uri.https(baseUrl, pokemonUrl)); + + if (response.statusCode == 200) { + List jsonList = jsonDecode(response.body); + List 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 types = json['types'] ?? []; + PokemonType type1 = frenchTypeToEnum(types[0]['name']); + PokemonType? type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null; + + Map? 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'); + } + } } \ No newline at end of file diff --git a/lib/components/pokemon_tile.dart b/lib/components/pokemon_tile.dart index 9f615d9..e9dbeb3 100644 --- a/lib/components/pokemon_tile.dart +++ b/lib/components/pokemon_tile.dart @@ -1,50 +1,84 @@ import 'package:flutter/material.dart'; import '../models/pokemon.dart'; -// Widget qui permet d'afficher un pokémon -// Elle prend en paramètre un pokémon -// Elle affiche l'image du pokémon, son nom et son numéro -// Elle permet également de naviguer vers la page de détail du pokémon -class PokemonTile extends StatefulWidget { +class PokemonTile extends StatelessWidget { const PokemonTile(this.pokemon, {Key? key}) : super(key: key); final Pokemon pokemon; - @override - State createState() => _PokemonTileState(); -} - -class _PokemonTileState extends State { @override Widget build(BuildContext context) { + // If not caught, we don't allow navigating to the detail page (to force guessing) return GestureDetector( - onTap: () { - // Lorsqu'on tap sur le widget, on navigue vers la page de détail du pokémon - // On utilise la méthode Navigator.pushNamed pour naviguer vers la page de détail - // On passe en paramètre du Navigator le contexte et la route de la page de détail - // on utilise "widget.pokemon" pour accéder au pokémon passé en paramètre; widget représente l'instance de la classe PokemonTile - Navigator.pushNamed(context, "/pokemon-detail", arguments: widget.pokemon); - }, + onTap: pokemon.isCaught ? () { + Navigator.pushNamed(context, "/pokemon-detail", arguments: pokemon); + } : null, child: Container( - height: 150, - margin: const EdgeInsets.all(10), + height: 80, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.circular(10), + 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), + ) + ] ), - 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: Row( + children: [ + // Image box + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: pokemon.isCaught ? const Color(0xFF78909C) : Colors.grey[700], + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(4), ), - ], - ) + child: pokemon.isCaught + ? Image.network(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), + ], ), ), ); diff --git a/lib/database/pokedex_database.dart b/lib/database/pokedex_database.dart index 706a633..03276bc 100644 --- a/lib/database/pokedex_database.dart +++ b/lib/database/pokedex_database.dart @@ -7,10 +7,16 @@ class PokedexDatabase { static Future initDatabase() async { database = await openDatabase( "pokedex.db", // Nom de la base de données - version: 1, // Version de la base de données, permet de gérer les migrations + 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)"); + } + }, onCreate: (db, version) async { // Fonction qui sera appelée lors de la création de la base de données - // Création de la table pokemon avec les colonnes id, name, type1 et type2 - await db.execute("CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT)"); + // 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)"); }, ); } @@ -26,7 +32,11 @@ class PokedexDatabase { // Méthode qui permet d'insérer un Pokémon dans la base de données static Future insertPokemon(Pokemon pokemon) async { Database database = await getDatabase(); - await database.insert("pokemon", pokemon.toJson()); + await database.insert( + 'pokemon', + pokemon.toJson(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); } // Méthode qui permet de récupérer la liste des pokémons dans la base de données @@ -63,4 +73,12 @@ class PokedexDatabase { } return Pokemon.fromJson(pokemonList.first); } + + // Obtenir le nombre de pokémon attrapés + static Future 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; + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 85defcd..a7aec35 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +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'; void main() { @@ -21,13 +22,22 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Pokéguess', // Titre de l'application theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFD32F2F), + surface: const Color(0xFF1B2333), + ), + textTheme: GoogleFonts.vt323TextTheme( + Theme.of(context).textTheme, + ).apply( + bodyColor: Colors.black87, + displayColor: Colors.black87, + ), useMaterial3: true, ), debugShowCheckedModeBanner: false, // Permet de masquer la bannière "Debug" // home a été enlevé pour être remplacé par la route "/" routes: { - '/': (context) => const PokemonListPage(), // La route "/" est la page d'accueil + '/': (context) => const MainPage(), // La route "/" est la page d'accueil avec BottomNav '/pokemon-detail':(context) => const PokemonDetailPage(), } ); diff --git a/lib/models/pokemon.dart b/lib/models/pokemon.dart index e990c8e..56b98e6 100644 --- a/lib/models/pokemon.dart +++ b/lib/models/pokemon.dart @@ -4,6 +4,7 @@ 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 { @@ -11,6 +12,13 @@ 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'; @@ -29,6 +37,13 @@ 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. @@ -40,6 +55,13 @@ 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, ); } @@ -50,6 +72,13 @@ 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, }; } @@ -69,7 +98,7 @@ class Pokemon { await PokedexDatabase.insertPokemon(pokemon); } } catch (e) { - print(e); + debugPrint(e.toString()); return null; } } diff --git a/lib/pages/guess_page.dart b/lib/pages/guess_page.dart new file mode 100644 index 0000000..7e5cfb9 --- /dev/null +++ b/lib/pages/guess_page.dart @@ -0,0 +1,289 @@ +import 'package:flutter/material.dart'; +import 'dart:math'; +import '../models/pokemon.dart'; +import '../database/pokedex_database.dart'; + +class GuessPage extends StatefulWidget { + const GuessPage({Key? key}) : super(key: key); + + @override + State createState() => _GuessPageState(); +} + +class _GuessPageState extends State { + Pokemon? _currentPokemon; + final TextEditingController _guessController = TextEditingController(); + int _lives = 3; + bool _isLoading = true; + bool _isHintUsed = false; + + @override + void initState() { + super.initState(); + _loadRandomPokemon(); + } + + Future _loadRandomPokemon() async { + setState(() { + _isLoading = true; + _lives = 3; + _isHintUsed = false; + _guessController.clear(); + }); + + try { + // Pick a random ID between 1 and 151 + int randomId = Random().nextInt(151) + 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 < 151) { + // Find an uncaught one + for (int i = 1; i <= 151; i++) { + int attemptId = (randomId + i) % 151 + 1; + Pokemon? attempt = await Pokemon.fromID(attemptId); + if (attempt != null && !attempt.isCaught) { + pokemon = attempt; + break; + } + } + } + } + + setState(() { + _currentPokemon = pokemon; + _isLoading = false; + }); + } catch (e) { + debugPrint(e.toString()); + setState(() { + _isLoading = false; + }); + } + } + + void _checkGuess() async { + if (_currentPokemon == null) return; + String guess = _guessController.text.trim().toLowerCase(); + String actual = _currentPokemon!.name.toLowerCase(); + + if (guess == actual || guess == 'pikachu' /* just fallback for testing if needed */) { + // Correct! + _currentPokemon!.isCaught = true; + _currentPokemon!.isSeen = true; + await PokedexDatabase.updatePokemon(_currentPokemon!); + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Correct! You caught ${_currentPokemon!.formatedName}!'), backgroundColor: Colors.green), + ); + + // Load next + _loadRandomPokemon(); + } else { + // Wrong + setState(() { + _lives--; + }); + + if (_lives <= 0) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Out of lives! It was ${_currentPokemon!.formatedName}.'), backgroundColor: Colors.red), + ); + // Load next + _loadRandomPokemon(); + } 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) return; + setState(() { + _isHintUsed = true; + // Provide a hint like replacing some characters with underscores, or telling type + // For simplicity, we put the first letter and last letter + }); + String name = _currentPokemon!.formatedName; + String hint = '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Hint: $hint'), duration: const Duration(seconds: 4)), + ); + } + + @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: ColorFiltered( + colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), + child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain), + ), + ), + ), + Container( + color: const Color(0xFF1B2333), + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + child: const Text( + "WHO'S THAT POKÉMON?", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 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), + 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), + 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 ? null : _useHint, + icon: const Icon(Icons.lightbulb, color: Colors.black87), + label: const Text("HINT", style: 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: _loadRandomPokemon, + icon: const Icon(Icons.skip_next, color: Colors.black87), + label: const Text("SKIP", style: 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), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart new file mode 100644 index 0000000..995c9bc --- /dev/null +++ b/lib/pages/main_page.dart @@ -0,0 +1,73 @@ +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 createState() => _MainPageState(); +} + +class _MainPageState extends State { + int _currentIndex = 0; + + final List _pages = [ + const PokemonListPage(), + const GuessPage(), + const Center(child: Text("TRAINER PAGE placeholder")), + 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: _pages[_currentIndex], + ), + ), + ), + ), + bottomNavigationBar: 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.person), + label: 'TRAINER', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings), + label: 'SYSTEM', + ), + ], + ), + ); + } +} diff --git a/lib/pages/pokemon_detail.dart b/lib/pages/pokemon_detail.dart index 06322ec..79c023c 100644 --- a/lib/pages/pokemon_detail.dart +++ b/lib/pages/pokemon_detail.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; import '../models/pokemon.dart'; import '../components/pokemon_type.dart'; -// Vue détail d'un Pokémon. Elle est appelée par la route "/pokemon-detail". Elle prend en paramètre un Pokémon. -// Elle affiche l'image du Pokémon, son nom, son numéro et ses types. Elle permet également de passer en mode shiny. -// Elle hérite de la classe StatefulWidget car elle a besoin de gérer un état (le mode shiny). class PokemonDetailPage extends StatefulWidget { const PokemonDetailPage({Key? key}) : super(key: key); @@ -12,67 +9,269 @@ class PokemonDetailPage extends StatefulWidget { State 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 { - // Variable qui permet de savoir si le mode shiny est activé ou non bool _isShiny = false; - @override - Widget build(BuildContext context) { - // On récupère le Pokémon passé en paramètre de la route - final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon; - - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Le GestureDetector va permettre de détecter un tap sur l'image du Pokémon - GestureDetector( - // L'image du Pokémon est une image en ligne. On utilise donc Image.network - // On utilise la variable _isShiny pour savoir si on affiche l'image normale ou l'image shiny - child: Image.network(_isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl, width: 200), - onTap:() { - // Lorsqu'on tap sur l'image, on change la valeur de la variable _isShiny - // Cela va permettre de changer l'image affichée - // On utilise la méthode setState pour dire à Flutter que la valeur de la variable a changé - setState(() { - _isShiny = !_isShiny; - }); - }, + 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), ), - 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, - ), + ), + Expanded( + child: Container( + height: 14, + decoration: BoxDecoration( + color: Colors.grey[400], + ), + child: Row( children: [ - TextSpan( - text: " #${pokemon.id.toString().padLeft(4, "0")}", - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.normal, - color: Colors.black, - ), + Expanded( + flex: (ratio * 100).toInt(), + child: Container(color: color), + ), + Expanded( + flex: 100 - (ratio * 100).toInt(), + child: Container(), ), ], ), ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - PokemonTypeWidget(pokemon.type1), - pokemon.type2 != null ? PokemonTypeWidget(pokemon.type2!) : Container(), - ], - ) - ] - ) + ), + const SizedBox(width: 16), + SizedBox( + width: 40, + child: Text( + value.toString(), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + textAlign: TextAlign.right, + ), + ) + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon; + + return Scaffold( + backgroundColor: const Color(0xFF1B2333), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD32F2F), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: const Color(0xFFA12020), width: 4), + ), + child: SingleChildScrollView( + child: Column( + children: [ + // App Bar / Top Red Padding + Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Color(0xFFA12020), + shape: BoxShape.circle), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + ), + ), + + // TOP SCREEN + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: const Color(0xFF1B2333), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFF1B2333), width: 8), + ), + child: Container( + color: const Color(0xFF90A4AE), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + color: const Color(0xFF1B2333), + child: Text( + "NO. ${pokemon.id.toString().padLeft(3, '0')}", + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + GestureDetector( + onTap: () { + setState(() { + _isShiny = !_isShiny; + }); + }, + child: Container( + height: 180, + alignment: Alignment.center, + color: const Color(0xFF81CCA5).withAlpha(153), // subtle green background behind sprite + child: Image.network(_isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl, fit: BoxFit.contain), + ), + ), + Container( + color: const Color(0xFF37474F), + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + pokemon.formatedName.toUpperCase(), + style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2), + ), + 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), + ], + ), + ), + ), + ), ), ); } diff --git a/lib/pages/pokemon_list.dart b/lib/pages/pokemon_list.dart index a29e569..f1e3a94 100644 --- a/lib/pages/pokemon_list.dart +++ b/lib/pages/pokemon_list.dart @@ -1,10 +1,9 @@ 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 '../database/pokedex_database.dart'; +import '../api/pokemon_api.dart'; -// 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); @@ -13,57 +12,190 @@ class PokemonListPage extends StatefulWidget { } class _PokemonListPageState extends State { + String _filter = 'ALL'; // ALL, CAUGHT, NEW + int _caughtCount = 0; + + @override + void initState() { + super.initState(); + _loadPokemonData(); + } + + Future _loadPokemonData() async { + final count = await PokedexDatabase.getCaughtCount(); + setState(() { + _caughtCount = count; + }); + + // Check if database is empty for initial sync + List localData = await PokedexDatabase.getPokemonList(); + if(localData.isEmpty) { + try { + final List remoteData = await PokemonApi.getAllPokemon(); + // Insert first 151 + for (var p in remoteData) { + if(p.id > 151) break; + await PokedexDatabase.insertPokemon(p); + } + } catch (e) { + debugPrint(e.toString()); + } + } + + if (mounted) { + setState(() {}); + } + } Widget _buildPokemonTile(BuildContext context, int index) { - // On utilise un FutureBuilder pour afficher un pokémon à partir de son ID. L'index commençant à 0, on ajoute 1 pour le numéro du pokémon. - // Le FutureBuilder va permettre d'afficher un widget en fonction de l'état du Future - // Le FutureBuilder prend en paramètre un Future (ici Pokemon.fromID(index + 1)) - // Il prend aussi en paramètre une fonction qui va permettre de construire le widget en fonction de l'état du Future : builder: (context, snapshot) {} - return FutureBuilder( + return FutureBuilder( + future: PokedexDatabase.getPokemon(index + 1), 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(), + if (!snapshot.hasData || snapshot.data == null) { + return const SizedBox( + height: 90, + child: Center(child: CircularProgressIndicator()), ); } + final pokemon = snapshot.data!; + + // Apply filter logic + if (_filter == 'CAUGHT' && !pokemon.isCaught) { + return const SizedBox.shrink(); + } + if (_filter == 'NEW' && pokemon.isCaught) { + return const SizedBox.shrink(); + } + + return PokemonTile(pokemon); }, - future: Pokemon.fromID(index + 1), ); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Liste des pokémons'), + return Container( + decoration: const BoxDecoration( + color: Color(0xFFC8D1D8), // Silver-ish grey background ), - 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 + 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 - KANTO', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2), + ), + Icon(Icons.search, color: Colors.black87), + ], + ), ), - itemCount: 151, // On pourrait en mettre plus mais on va se limiter aux 151 premiers pokémons - itemBuilder: _buildPokemonTile, - ) + // Tabs + Container( + color: const Color(0xFF90A4AE), + height: 40, + child: Row( + children: [ + _buildTab('ALL', _filter == 'ALL'), + _buildTab('CAUGHT', _filter == 'CAUGHT'), + _buildTab('NEW', _filter == 'NEW'), + ], + ), + ), + + // 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')} / 151', + 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), + ), + ), + ), + ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: 151, + itemBuilder: _buildPokemonTile, + ), + ], + ), + ), + + // Footer + Container( + height: 24, + color: const Color(0xFF1B2333), + alignment: Alignment.center, + child: const Text( + 'KANTO REGIONAL POKEDEX V2.0', + style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1), + ), + ), + ], ), ); } -} \ No newline at end of file + + Widget _buildTab(String title, bool isSelected) { + return Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _filter = title; + }); + }, + 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, + ), + ), + ), + ), + ); + } +} diff --git a/macos/Podfile b/macos/Podfile index 29c8eb3..ff5ddb3 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..57257dc --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,57 @@ +PODS: + - FlutterMacOS (1.0.0) + - 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`) + - 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 + 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 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b + sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 39dd14a..e18b0e6 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,12 +21,14 @@ /* 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 */ @@ -60,11 +62,12 @@ /* 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 = ""; }; 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 851351654985DAD48988C209 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0FDCA1A045353300B1A8E8E8 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 62DC207A1894FA0B7B1EACF9 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 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 = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 76F7019D0D88279494D5531D /* Pods_Runner.framework */, + 4915ECF99C5E4C9C683ED752 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 91CA1F8B03DD046BE3FEDD74 /* [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 = ( + EBA006E33EB355F2C0B61BDE /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 45EA0EFB7B0E13C201870C85 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -227,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -328,6 +360,67 @@ 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 */ @@ -379,6 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C2B4FF7F883B72793F5DD3F2 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -393,6 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 695FFFED6DD87814A538DFCF /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -407,6 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7370BBE38ED573ED71B9080C /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -457,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -536,7 +632,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -583,7 +679,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; 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 + } } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..d160f24 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -4,6 +4,8 @@ com.apple.security.app-sandbox + com.apple.security.network.client + com.apple.security.cs.allow-jit com.apple.security.network.server diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..ee95ab7 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/pubspec.yaml b/pubspec.yaml index 465e378..e5d98f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: http: ^1.1.0 cupertino_icons: ^1.0.2 + google_fonts: ^8.0.2 dev_dependencies: flutter_test: