From fbf37e6861448d5a9a3845ad524b3074bb89dce6 Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Tue, 17 Mar 2026 14:57:39 +0100 Subject: [PATCH] chore: major changes --- devtools_options.yaml | 3 + ios/Podfile.lock | 7 + lib/api/pokemon_api.dart | 2 + lib/components/pokemon_type.dart | 4 +- lib/database/pokedex_database.dart | 5 + lib/pages/guess_page.dart | 139 +++++++++++++++--- lib/pages/main_page.dart | 64 ++++---- lib/pages/pokemon_detail.dart | 10 +- lib/pages/pokemon_list.dart | 110 +++++++++----- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 7 + pubspec.yaml | 1 + 12 files changed, 257 insertions(+), 97 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 08957a1..10cbf8e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,8 @@ PODS: - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -31,6 +34,7 @@ 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`) @@ -41,6 +45,8 @@ 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: @@ -48,6 +54,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 diff --git a/lib/api/pokemon_api.dart b/lib/api/pokemon_api.dart index bb32c4f..f049038 100644 --- a/lib/api/pokemon_api.dart +++ b/lib/api/pokemon_api.dart @@ -13,6 +13,7 @@ class PokemonApi { static const String pokemonUrl = 'api/v1/pokemon'; static Future 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")); @@ -59,6 +60,7 @@ class PokemonApi { } static Future> getAllPokemon() async { + print('API Call: Fetching ALL Pokémon from Tyradex...'); final response = await http.get(Uri.https(baseUrl, pokemonUrl)); if (response.statusCode == 200) { diff --git a/lib/components/pokemon_type.dart b/lib/components/pokemon_type.dart index ddec327..6afd910 100644 --- a/lib/components/pokemon_type.dart +++ b/lib/components/pokemon_type.dart @@ -16,8 +16,8 @@ class PokemonTypeWidget extends StatelessWidget { Color typeColor = typeToColor(type); return Container( - padding: const EdgeInsets.all(15), - margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + margin: const EdgeInsets.only(right: 6), alignment: Alignment.center, decoration: BoxDecoration( color: typeColor, diff --git a/lib/database/pokedex_database.dart b/lib/database/pokedex_database.dart index 03276bc..39d734c 100644 --- a/lib/database/pokedex_database.dart +++ b/lib/database/pokedex_database.dart @@ -1,9 +1,12 @@ +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 onDatabaseUpdate = ValueNotifier(0); + static Future initDatabase() async { database = await openDatabase( "pokedex.db", // Nom de la base de données @@ -37,6 +40,7 @@ class PokedexDatabase { pokemon.toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); + onDatabaseUpdate.value++; } // Méthode qui permet de récupérer la liste des pokémons dans la base de données @@ -62,6 +66,7 @@ class PokedexDatabase { static Future 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 diff --git a/lib/pages/guess_page.dart b/lib/pages/guess_page.dart index 7e5cfb9..7258d4b 100644 --- a/lib/pages/guess_page.dart +++ b/lib/pages/guess_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'dart:math'; +import 'package:shared_preferences/shared_preferences.dart'; import '../models/pokemon.dart'; import '../database/pokedex_database.dart'; @@ -16,34 +17,56 @@ class _GuessPageState extends State { int _lives = 3; bool _isLoading = true; bool _isHintUsed = false; + bool _isShiny = false; + int _currentScore = 0; + int _bestScore = 0; @override void initState() { super.initState(); + _loadBestScore(); _loadRandomPokemon(); } + Future _loadBestScore() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _bestScore = prefs.getInt('best_score') ?? 0; + }); + } + + Future _saveBestScore() async { + if (_currentScore > _bestScore) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('best_score', _currentScore); + setState(() { + _bestScore = _currentScore; + }); + } + } + Future _loadRandomPokemon() async { setState(() { _isLoading = true; _lives = 3; _isHintUsed = false; + _isShiny = Random().nextInt(10) == 0; // 10% chance for shiny _guessController.clear(); }); try { - // Pick a random ID between 1 and 151 - int randomId = Random().nextInt(151) + 1; + // 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 < 151) { + if (count < 1025) { // Find an uncaught one - for (int i = 1; i <= 151; i++) { - int attemptId = (randomId + i) % 151 + 1; + 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; @@ -53,15 +76,19 @@ class _GuessPageState extends State { } } - setState(() { - _currentPokemon = pokemon; - _isLoading = false; - }); + if (mounted) { + setState(() { + _currentPokemon = pokemon; + _isLoading = false; + }); + } } catch (e) { debugPrint(e.toString()); - setState(() { - _isLoading = false; - }); + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } @@ -70,26 +97,49 @@ class _GuessPageState extends State { String guess = _guessController.text.trim().toLowerCase(); String actual = _currentPokemon!.name.toLowerCase(); - if (guess == actual || guess == 'pikachu' /* just fallback for testing if needed */) { + // 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; + }); + } + await _saveBestScore(); + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Correct! You caught ${_currentPokemon!.formatedName}!'), backgroundColor: Colors.green), + SnackBar( + content: Text(_isShiny + ? '✨ SHINY! You caught ${_currentPokemon!.formatedName}! (+20 pts) ✨' + : 'Correct! You caught ${_currentPokemon!.formatedName}!'), + backgroundColor: _isShiny ? Colors.amber[800] : Colors.green + ), ); // Load next _loadRandomPokemon(); } else { // Wrong - setState(() { - _lives--; - }); + if (mounted) { + setState(() { + _lives--; + }); + } if (_lives <= 0) { + if (mounted) { + setState(() { + _currentScore = 0; // Reset score only when all lives are lost + }); + } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Out of lives! It was ${_currentPokemon!.formatedName}.'), backgroundColor: Colors.red), @@ -119,6 +169,17 @@ class _GuessPageState extends State { ); } + 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) { @@ -165,7 +226,10 @@ class _GuessPageState extends State { child: Padding( padding: const EdgeInsets.all(16.0), child: ColorFiltered( - colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), + colorFilter: ColorFilter.mode( + _isShiny ? Colors.yellow[700]! : Colors.black, + BlendMode.srcIn + ), child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain), ), ), @@ -174,12 +238,12 @@ class _GuessPageState extends State { color: const Color(0xFF1B2333), width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text( - "WHO'S THAT POKÉMON?", + child: Text( + _isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?", textAlign: TextAlign.center, style: TextStyle( - color: Colors.white, - fontSize: 22, + color: _isShiny ? Colors.yellow[400] : Colors.white, + fontSize: _isShiny ? 18 : 22, fontWeight: FontWeight.bold, letterSpacing: 2, ), @@ -275,10 +339,39 @@ class _GuessPageState extends State { ), ], ), + 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: 24), + const SizedBox(height: 32), ], ), ), diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 995c9bc..2465e7f 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -15,7 +15,6 @@ class _MainPageState extends State { final List _pages = [ const PokemonListPage(), const GuessPage(), - const Center(child: Text("TRAINER PAGE placeholder")), const Center(child: Text("SYSTEM PAGE placeholder")), ]; @@ -34,39 +33,44 @@ class _MainPageState extends State { ), child: ClipRRect( borderRadius: BorderRadius.circular(26), - child: _pages[_currentIndex], + child: IndexedStack( + index: _currentIndex, + children: _pages, + ), ), ), ), ), - 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', - ), - ], + 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', + ), + ], + ), ), ); } diff --git a/lib/pages/pokemon_detail.dart b/lib/pages/pokemon_detail.dart index 79c023c..5ff291d 100644 --- a/lib/pages/pokemon_detail.dart +++ b/lib/pages/pokemon_detail.dart @@ -135,10 +135,14 @@ class _PokemonDetailPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - pokemon.formatedName.toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2), + 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), diff --git a/lib/pages/pokemon_list.dart b/lib/pages/pokemon_list.dart index f1e3a94..792ab34 100644 --- a/lib/pages/pokemon_list.dart +++ b/lib/pages/pokemon_list.dart @@ -14,62 +14,76 @@ class PokemonListPage extends StatefulWidget { class _PokemonListPageState extends State { String _filter = 'ALL'; // ALL, CAUGHT, NEW int _caughtCount = 0; + List _allPokemon = []; + List _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 _loadPokemonData() async { + setState(() => _isSyncing = true); + 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 + // Insert all for (var p in remoteData) { - if(p.id > 151) break; await PokedexDatabase.insertPokemon(p); } + localData = await PokedexDatabase.getPokemonList(); } catch (e) { debugPrint(e.toString()); } } + + // Sort by ID to ensure order + localData.sort((a, b) => a.id.compareTo(b.id)); if (mounted) { - setState(() {}); + 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) { - return FutureBuilder( - future: PokedexDatabase.getPokemon(index + 1), - builder: (context, snapshot) { - 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); - }, - ); + final pokemon = _filteredPokemon[index]; + return PokemonTile(pokemon); } @override @@ -89,7 +103,7 @@ class _PokemonListPageState extends State { children: [ Icon(Icons.menu, color: Colors.black87), Text( - 'LIST - KANTO', + 'LIST - NATIONAL', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2), ), Icon(Icons.search, color: Colors.black87), @@ -104,7 +118,6 @@ class _PokemonListPageState extends State { children: [ _buildTab('ALL', _filter == 'ALL'), _buildTab('CAUGHT', _filter == 'CAUGHT'), - _buildTab('NEW', _filter == 'NEW'), ], ), ), @@ -119,7 +132,7 @@ class _PokemonListPageState extends State { child: Column( children: [ Text( - '${_caughtCount.toString().padLeft(3, '0')} / 151', + '${_caughtCount.toString().padLeft(3, '0')} / ${_allPokemon.length}', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), ), const Text( @@ -146,11 +159,29 @@ class _PokemonListPageState extends State { ), ), ), - ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: 151, - itemBuilder: _buildPokemonTile, - ), + 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, + ), ], ), ), @@ -161,7 +192,7 @@ class _PokemonListPageState extends State { color: const Color(0xFF1B2333), alignment: Alignment.center, child: const Text( - 'KANTO REGIONAL POKEDEX V2.0', + 'NATIONAL POKEDEX V2.0', style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1), ), ), @@ -174,9 +205,10 @@ class _PokemonListPageState extends State { return Expanded( child: GestureDetector( onTap: () { - setState(() { + if (_filter != title) { _filter = title; - }); + _applyFilter(); + } }, child: Container( decoration: BoxDecoration( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6bfb7b3..b031e41 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ 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")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 57257dc..881f999 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,5 +1,8 @@ PODS: - FlutterMacOS (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -31,6 +34,7 @@ PODS: 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`) @@ -41,6 +45,8 @@ SPEC REPOS: 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: @@ -48,6 +54,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 diff --git a/pubspec.yaml b/pubspec.yaml index e5d98f6..dc1cb1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: http: ^1.1.0 cupertino_icons: ^1.0.2 google_fonts: ^8.0.2 + shared_preferences: ^2.5.4 dev_dependencies: flutter_test: