From 3984e94bc50624e0f0811549b29da7fd59badefe Mon Sep 17 00:00:00 2001 From: Echalaye Date: Tue, 23 Jun 2026 14:17:27 +0200 Subject: [PATCH] patch small issue with trad and theme --- .../datasources/pokemon_local_datasource.dart | 41 +++++--- .../pokemon_remote_datasource.dart | 34 +++++-- lib/data/dto/pokemon_dto.dart | 62 +++++++++--- .../repositories/pokemon_repository_impl.dart | 3 + lib/domain/entities/pokemon.dart | 12 +++ .../repositories/pokemon_repository.dart | 3 + lib/l10n/app_en.arb | 15 ++- lib/l10n/app_fr.arb | 15 ++- lib/l10n/app_localizations.dart | 60 ++++++++++++ lib/l10n/app_localizations_en.dart | 30 ++++++ lib/l10n/app_localizations_fr.dart | 30 ++++++ lib/presentation/pages/game_over_page.dart | 24 +++-- lib/presentation/pages/guess_page.dart | 5 +- lib/presentation/pages/main_page.dart | 9 +- lib/presentation/pages/pokemon_detail.dart | 65 +++++++++---- lib/presentation/pages/pokemon_list.dart | 12 ++- lib/presentation/providers/game_provider.dart | 4 +- .../providers/gen_filter_provider.dart | 9 +- .../providers/pokedex_provider.dart | 2 +- .../providers/repository_provider.dart | 10 +- .../providers/theme_provider.dart | 20 ++-- lib/presentation/theme/type_colors.dart | 37 ++++++- .../widgets/detail/pokemon_detail_top.dart | 18 +++- .../widgets/detail/pokemon_stats_panel.dart | 97 ++++++++++++++----- .../widgets/game_over/game_over_actions.dart | 14 ++- .../widgets/game_over/game_over_header.dart | 19 ++-- .../widgets/game_over/hinge_divider.dart | 11 +-- .../widgets/guess/gen_filter_section.dart | 17 ++-- .../widgets/guess/guess_silhouette.dart | 6 +- .../widgets/list/pokedex_list_header.dart | 12 +-- lib/presentation/widgets/pokemon_type.dart | 3 +- .../widgets/system/language_picker.dart | 7 +- .../widgets/system/palette_picker.dart | 6 +- 33 files changed, 556 insertions(+), 156 deletions(-) diff --git a/lib/data/datasources/pokemon_local_datasource.dart b/lib/data/datasources/pokemon_local_datasource.dart index c08d686..5283626 100644 --- a/lib/data/datasources/pokemon_local_datasource.dart +++ b/lib/data/datasources/pokemon_local_datasource.dart @@ -2,24 +2,43 @@ import 'package:sqflite_common/sqflite.dart'; import '../../domain/entities/pokemon.dart'; import '../dto/pokemon_dto.dart'; -/// Accès SQLite local au Pokédex. Schéma et migrations identiques à l'ancien PokedexDatabase. +const _createSql = ''' + CREATE TABLE IF NOT EXISTS pokemon ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + name_fr TEXT, + name_en TEXT, + type1 TEXT NOT NULL, + type2 TEXT, + hp INTEGER NOT NULL, + atk INTEGER NOT NULL, + def INTEGER NOT NULL, + spd INTEGER NOT NULL, + description TEXT, + description_en TEXT, + isCaught INTEGER NOT NULL DEFAULT 0, + isSeen INTEGER NOT NULL DEFAULT 0 + ) +'''; + class PokemonLocalDataSource { + final String languageCode; + + PokemonLocalDataSource({this.languageCode = 'fr'}); + Future? _dbFuture; Future _getDb() { return _dbFuture ??= openDatabase( 'pokedex.db', - version: 2, + version: 5, 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)'); - } + // Drop and recreate on any upgrade to pick up new columns cleanly. + await db.execute('DROP TABLE IF EXISTS pokemon'); + await db.execute(_createSql); }, onCreate: (db, version) async { - await db.execute( - '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)'); + await db.execute(_createSql); }, ); } @@ -27,14 +46,14 @@ class PokemonLocalDataSource { Future> getAll() async { final db = await _getDb(); final rows = await db.query('pokemon'); - return rows.map(PokemonDto.fromDb).toList(); + return rows.map((r) => PokemonDto.fromDb(r, languageCode: languageCode)).toList(); } Future getById(int id) async { final db = await _getDb(); final rows = await db.query('pokemon', where: 'id = ?', whereArgs: [id]); if (rows.isEmpty) return null; - return PokemonDto.fromDb(rows.first); + return PokemonDto.fromDb(rows.first, languageCode: languageCode); } Future saveAll(List pokemons) async { diff --git a/lib/data/datasources/pokemon_remote_datasource.dart b/lib/data/datasources/pokemon_remote_datasource.dart index de3a70a..e99435c 100644 --- a/lib/data/datasources/pokemon_remote_datasource.dart +++ b/lib/data/datasources/pokemon_remote_datasource.dart @@ -8,12 +8,13 @@ import '../dto/pokemon_dto.dart'; /// Accès distant à l'API Tyradex. class PokemonRemoteDataSource { final http.Client _client; + final String languageCode; - PokemonRemoteDataSource({http.Client? client}) + PokemonRemoteDataSource({http.Client? client, this.languageCode = 'fr'}) : _client = client ?? http.Client(); Future getById(int id) async { - AppLogger.info('API: fetching Pokémon $id'); + AppLogger.info('API: fetching Pokémon $id (lang: $languageCode)'); final response = await _client .get(Uri.https(AppConstants.apiBaseUrl, '${AppConstants.apiPokemonPath}/$id')); if (response.statusCode != 200) { @@ -21,11 +22,29 @@ class PokemonRemoteDataSource { 'Erreur récupération du pokémon $id, code ${response.statusCode}'); } final json = jsonDecode(response.body) as Map; - return PokemonDto.fromTyradexJson(json, fallbackId: id); + return PokemonDto.fromTyradexJson(json, + fallbackId: id, languageCode: languageCode); + } + + /// Fetches the English genus (category) from PokéAPI for a given Pokémon id. + /// Returns null on any error (network, missing entry, etc.). + Future getEnglishGenus(int id) async { + try { + final response = await _client + .get(Uri.https('pokeapi.co', '/api/v2/pokemon-species/$id/')); + if (response.statusCode != 200) return null; + final json = jsonDecode(response.body) as Map; + final genera = json['genera'] as List? ?? []; + for (final g in genera) { + final lang = (g['language'] as Map?)?['name']; + if (lang == 'en') return g['genus'] as String?; + } + } catch (_) {} + return null; } Future> getAll() async { - AppLogger.info('API: fetching ALL Pokémon'); + AppLogger.info('API: fetching ALL Pokémon (lang: $languageCode)'); final response = await _client .get(Uri.https(AppConstants.apiBaseUrl, AppConstants.apiPokemonPath)); if (response.statusCode != 200) { @@ -34,9 +53,12 @@ class PokemonRemoteDataSource { final List jsonList = jsonDecode(response.body); final result = []; for (final json in jsonList) { - if (json['pokedex_id'] == 0) continue; // entrée générique Tyradex + if (json['pokedex_id'] == 0) continue; try { - result.add(PokemonDto.fromTyradexJson(json as Map)); + result.add(PokemonDto.fromTyradexJson( + json as Map, + languageCode: languageCode, + )); } catch (e, st) { AppLogger.error('Parsing pokemon échoué: ${json['name']}', e, st); } diff --git a/lib/data/dto/pokemon_dto.dart b/lib/data/dto/pokemon_dto.dart index 5387ff4..56b5af2 100644 --- a/lib/data/dto/pokemon_dto.dart +++ b/lib/data/dto/pokemon_dto.dart @@ -4,8 +4,7 @@ import '../../domain/entities/pokemon.dart'; class PokemonDto { PokemonDto._(); - /// Mappe un nom de type français (API Tyradex) vers l'enum. - static PokemonType frenchTypeToEnum(String frenchType) { + static PokemonType _frenchTypeToEnum(String type) { const map = { 'Normal': PokemonType.normal, 'Combat': PokemonType.fighting, @@ -26,41 +25,65 @@ class PokemonDto { 'Ténèbres': PokemonType.dark, 'Fée': PokemonType.fairy, }; - return map[frenchType] ?? PokemonType.unknown; + return map[type] ?? PokemonType.unknown; } - /// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste). - /// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`. - static Pokemon fromTyradexJson(Map json, {int? fallbackId}) { + // Tyradex always returns type names in French regardless of endpoint language. + static PokemonType _typeToEnum(String typeName, String languageCode) { + return _frenchTypeToEnum(typeName); + } + + /// Construit une entité depuis la réponse Tyradex. + /// [languageCode] détermine quelle langue utiliser pour [name] ('fr' ou 'en'). + static Pokemon fromTyradexJson( + Map json, { + int? fallbackId, + String languageCode = 'fr', + }) { final id = (json['pokedex_id'] as int?) ?? fallbackId ?? (throw ArgumentError('pokedex_id absent et fallbackId non fourni')); + final nameMap = json['name'] as Map?; - final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown'; + final nameFr = nameMap?['fr'] as String? ?? 'unknown'; + final nameEn = nameMap?['en'] as String? ?? nameFr; + final name = languageCode == 'en' ? nameEn : nameFr; final List types = json['types'] ?? []; - final type1 = - types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown; - final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null; + // Tyradex retourne toujours les types dans la langue du endpoint (fr ou en). + final type1 = types.isNotEmpty + ? _typeToEnum(types[0]['name'] as String, languageCode) + : PokemonType.unknown; + final type2 = types.length > 1 + ? _typeToEnum(types[1]['name'] as String, languageCode) + : null; final Map? stats = json['stats']; + // Tyradex expose 'category' (FR) et parfois 'category_en' (EN). + final descFr = json['category'] as String?; + final descEn = (json['category_en'] ?? json['category']) as String?; return Pokemon( name: name, + nameFr: nameFr, + nameEn: nameEn, id: id, type1: type1, type2: type2, hp: stats?['hp'] ?? 0, atk: stats?['atk'] ?? 0, def: stats?['def'] ?? 0, - spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex - description: json['category'], + spd: stats?['vit'] ?? 0, + description: descFr, + descriptionEn: descEn, ); } - /// Sérialise pour SQLite. + /// Sérialise pour SQLite (stocke les deux noms et descriptions). static Map toDb(Pokemon p) { return { 'name': p.name, + 'name_fr': p.nameFr ?? p.name, + 'name_en': p.nameEn ?? p.name, 'id': p.id, 'type1': p.type1.name, 'type2': p.type2?.name, @@ -69,15 +92,23 @@ class PokemonDto { 'def': p.def, 'spd': p.spd, 'description': p.description, + 'description_en': p.descriptionEn, 'isCaught': p.isCaught ? 1 : 0, 'isSeen': p.isSeen ? 1 : 0, }; } /// Reconstruit depuis une ligne SQLite. - static Pokemon fromDb(Map row) { + /// [languageCode] détermine quel nom afficher. + static Pokemon fromDb(Map row, {String languageCode = 'fr'}) { + final nameFr = row['name_fr'] as String? ?? row['name'] as String; + final nameEn = row['name_en'] as String? ?? nameFr; + final name = languageCode == 'en' ? nameEn : nameFr; + return Pokemon( - name: row['name'], + name: name, + nameFr: nameFr, + nameEn: nameEn, id: row['id'], type1: PokemonType.values.firstWhere( (e) => e.name == row['type1'], @@ -94,6 +125,7 @@ class PokemonDto { def: row['def'] ?? 0, spd: row['spd'] ?? 0, description: row['description'], + descriptionEn: row['description_en'] as String?, isCaught: row['isCaught'] == 1 || row['isCaught'] == true, isSeen: row['isSeen'] == 1 || row['isSeen'] == true, ); diff --git a/lib/data/repositories/pokemon_repository_impl.dart b/lib/data/repositories/pokemon_repository_impl.dart index 7af383d..6064840 100644 --- a/lib/data/repositories/pokemon_repository_impl.dart +++ b/lib/data/repositories/pokemon_repository_impl.dart @@ -60,4 +60,7 @@ class PokemonRepositoryImpl implements PokemonRepository { @override Future seenCount() async => (await local?.seenCount()) ?? 0; + + @override + Future getEnglishGenus(int id) => remote.getEnglishGenus(id); } diff --git a/lib/domain/entities/pokemon.dart b/lib/domain/entities/pokemon.dart index eb73c79..5072c65 100644 --- a/lib/domain/entities/pokemon.dart +++ b/lib/domain/entities/pokemon.dart @@ -1,6 +1,8 @@ /// Entité métier représentant un Pokémon. Pure : aucun import Flutter / DB / API. class Pokemon { final String name; + final String? nameFr; + final String? nameEn; final int id; final PokemonType type1; final PokemonType? type2; @@ -9,11 +11,14 @@ class Pokemon { final int def; final int spd; final String? description; + final String? descriptionEn; final bool isCaught; final bool isSeen; const Pokemon({ required this.name, + this.nameFr, + this.nameEn, required this.id, required this.type1, this.type2, @@ -22,6 +27,7 @@ class Pokemon { required this.def, required this.spd, this.description, + this.descriptionEn, this.isCaught = false, this.isSeen = false, }); @@ -37,6 +43,8 @@ class Pokemon { Pokemon copyWith({ String? name, + String? nameFr, + String? nameEn, int? id, PokemonType? type1, PokemonType? type2, @@ -45,11 +53,14 @@ class Pokemon { int? def, int? spd, String? description, + String? descriptionEn, bool? isCaught, bool? isSeen, }) { return Pokemon( name: name ?? this.name, + nameFr: nameFr ?? this.nameFr, + nameEn: nameEn ?? this.nameEn, id: id ?? this.id, type1: type1 ?? this.type1, type2: type2 ?? this.type2, @@ -58,6 +69,7 @@ class Pokemon { def: def ?? this.def, spd: spd ?? this.spd, description: description ?? this.description, + descriptionEn: descriptionEn ?? this.descriptionEn, isCaught: isCaught ?? this.isCaught, isSeen: isSeen ?? this.isSeen, ); diff --git a/lib/domain/repositories/pokemon_repository.dart b/lib/domain/repositories/pokemon_repository.dart index 4aeedf3..5a06b0b 100644 --- a/lib/domain/repositories/pokemon_repository.dart +++ b/lib/domain/repositories/pokemon_repository.dart @@ -19,4 +19,7 @@ abstract interface class PokemonRepository { /// Nombre de Pokémon vus. Future seenCount(); + + /// Catégorie anglaise d'un Pokémon (ex. "Seed Pokémon") depuis PokéAPI. Null si indisponible. + Future getEnglishGenus(int id); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f79663d..941cd43 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,5 +53,18 @@ "statBestScore": "Best score", "statCaught": "Caught", "statSeenLabel": "Seen", - "statCompletion": "Completion" + "statCompletion": "Completion", + + "navList": "LIST", + "navGuess": "GUESS", + "navSystem": "SYSTEM", + + "langEnglish": "English", + "langFrench": "French", + + "palette0": "Pokédex Red", + "palette1": "Blue Ocean", + "palette2": "Green Forest", + "palette3": "Lightning Yellow", + "palette4": "Purple Shadow" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 34277a2..9011811 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -46,5 +46,18 @@ "statBestScore": "Meilleur score", "statCaught": "Attrapés", "statSeenLabel": "Vus", - "statCompletion": "Complétion" + "statCompletion": "Complétion", + + "navList": "LISTE", + "navGuess": "DEVINER", + "navSystem": "SYSTÈME", + + "langEnglish": "Anglais", + "langFrench": "Français", + + "palette0": "Pokédex Rouge", + "palette1": "Océan Bleu", + "palette2": "Forêt Verte", + "palette3": "Foudre Jaune", + "palette4": "Ombre Violette" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a4ec2e9..af1c29e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -349,6 +349,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Completion'** String get statCompletion; + + /// No description provided for @navList. + /// + /// In en, this message translates to: + /// **'LIST'** + String get navList; + + /// No description provided for @navGuess. + /// + /// In en, this message translates to: + /// **'GUESS'** + String get navGuess; + + /// No description provided for @navSystem. + /// + /// In en, this message translates to: + /// **'SYSTEM'** + String get navSystem; + + /// No description provided for @langEnglish. + /// + /// In en, this message translates to: + /// **'English'** + String get langEnglish; + + /// No description provided for @langFrench. + /// + /// In en, this message translates to: + /// **'French'** + String get langFrench; + + /// No description provided for @palette0. + /// + /// In en, this message translates to: + /// **'Pokédex Red'** + String get palette0; + + /// No description provided for @palette1. + /// + /// In en, this message translates to: + /// **'Blue Ocean'** + String get palette1; + + /// No description provided for @palette2. + /// + /// In en, this message translates to: + /// **'Green Forest'** + String get palette2; + + /// No description provided for @palette3. + /// + /// In en, this message translates to: + /// **'Lightning Yellow'** + String get palette3; + + /// No description provided for @palette4. + /// + /// In en, this message translates to: + /// **'Purple Shadow'** + String get palette4; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6863505..75d5690 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -148,4 +148,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get statCompletion => 'Completion'; + + @override + String get navList => 'LIST'; + + @override + String get navGuess => 'GUESS'; + + @override + String get navSystem => 'SYSTEM'; + + @override + String get langEnglish => 'English'; + + @override + String get langFrench => 'French'; + + @override + String get palette0 => 'Pokédex Red'; + + @override + String get palette1 => 'Blue Ocean'; + + @override + String get palette2 => 'Green Forest'; + + @override + String get palette3 => 'Lightning Yellow'; + + @override + String get palette4 => 'Purple Shadow'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index a82f5ba..3b3be91 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -148,4 +148,34 @@ class AppLocalizationsFr extends AppLocalizations { @override String get statCompletion => 'Complétion'; + + @override + String get navList => 'LISTE'; + + @override + String get navGuess => 'DEVINER'; + + @override + String get navSystem => 'SYSTÈME'; + + @override + String get langEnglish => 'Anglais'; + + @override + String get langFrench => 'Français'; + + @override + String get palette0 => 'Pokédex Rouge'; + + @override + String get palette1 => 'Océan Bleu'; + + @override + String get palette2 => 'Forêt Verte'; + + @override + String get palette3 => 'Foudre Jaune'; + + @override + String get palette4 => 'Ombre Violette'; } diff --git a/lib/presentation/pages/game_over_page.dart b/lib/presentation/pages/game_over_page.dart index 70d98f1..63b71b6 100644 --- a/lib/presentation/pages/game_over_page.dart +++ b/lib/presentation/pages/game_over_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/config/app_constants.dart'; import '../../l10n/app_localizations.dart'; import '../providers/repository_provider.dart'; +import '../providers/theme_provider.dart'; import '../widgets/game_over/game_over_header.dart'; import '../widgets/game_over/game_over_stats.dart'; import '../widgets/game_over/game_over_actions.dart'; @@ -44,12 +45,14 @@ class _GameOverPageState extends ConsumerState { final int streak = args?['streak'] ?? 0; final int score = args?['score'] ?? 0; - const darkRed = Color(0xFF9E1B1B); + final palette = appPalettes[ref.watch(themeProvider)]; + final primaryDark = HSLColor.fromColor(palette.primary) + .withLightness((HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0)) + .toColor(); const silverBg = Color(0xFFC8D1D8); - const messageBoxBg = Color(0xFF1B2333); return Scaffold( - backgroundColor: const Color(0xFFD32F2F), + backgroundColor: palette.primary, body: _isLoading ? const Center(child: CircularProgressIndicator(color: Colors.white)) : SingleChildScrollView( @@ -57,10 +60,15 @@ class _GameOverPageState extends ConsumerState { padding: const EdgeInsets.all(16.0), child: Column( children: [ - GameOverHeader(pokemonImage: pokemonImage, pokemonName: pokemonName), - const HingeDivider(), + GameOverHeader( + pokemonImage: pokemonImage, + pokemonName: pokemonName, + primaryDark: primaryDark, + surfaceColor: palette.surface, + ), + HingeDivider(color: primaryDark), Container( - decoration: BoxDecoration(color: darkRed, border: Border.all(color: darkRed, width: 4)), + decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -69,7 +77,7 @@ class _GameOverPageState extends ConsumerState { children: [ Container( width: double.infinity, - color: messageBoxBg, + color: palette.surface, padding: const EdgeInsets.all(24), child: Text( AppLocalizations.of(context)!.gameOverMessage, @@ -85,6 +93,8 @@ class _GameOverPageState extends ConsumerState { ), const SizedBox(height: 16), GameOverActions( + primaryColor: palette.primary, + primaryDark: primaryDark, onTryAgain: () => Navigator.pop(context, true), onBack: () => Navigator.pop(context, false), ), diff --git a/lib/presentation/pages/guess_page.dart b/lib/presentation/pages/guess_page.dart index be04e76..a2da277 100644 --- a/lib/presentation/pages/guess_page.dart +++ b/lib/presentation/pages/guess_page.dart @@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart'; import '../providers/gen_filter_provider.dart'; import '../providers/game_provider.dart'; import '../providers/navigation_provider.dart'; +import '../providers/theme_provider.dart'; import '../widgets/scanline_overlay.dart'; import '../widgets/guess/guess_silhouette.dart'; import '../widgets/guess/gen_filter_section.dart'; @@ -92,6 +93,7 @@ class _GuessPageState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(gameProvider); + final palette = appPalettes[ref.watch(themeProvider)]; if (state.status == GameStatus.loading) { return const Center(child: CircularProgressIndicator()); @@ -112,12 +114,13 @@ class _GuessPageState extends ConsumerState { SingleChildScrollView( child: Column( children: [ - GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed), + GuessSilhouette(pokemon: pokemon, isShiny: state.isShiny, isGuessed: isGuessed, surfaceColor: palette.surface), GenFilterSection( isOpen: _genFilterOpen, onToggleOpen: () => setState(() => _genFilterOpen = !_genFilterOpen), selectedGens: ref.watch(genFilterProvider), onToggle: (i) => ref.read(genFilterProvider.notifier).toggle(i), + surfaceColor: palette.surface, ), const SizedBox(height: 12), LivesRow(lives: state.lives), diff --git a/lib/presentation/pages/main_page.dart b/lib/presentation/pages/main_page.dart index 3bffdd9..c02979b 100644 --- a/lib/presentation/pages/main_page.dart +++ b/lib/presentation/pages/main_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../l10n/app_localizations.dart'; import '../providers/navigation_provider.dart'; import '../providers/theme_provider.dart'; import 'pokemon_list.dart'; @@ -56,10 +57,10 @@ class MainPage extends ConsumerWidget { type: BottomNavigationBarType.fixed, selectedItemColor: palette.primary, 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'), + items: [ + BottomNavigationBarItem(icon: const Icon(Icons.grid_view), label: AppLocalizations.of(context)!.navList), + BottomNavigationBarItem(icon: const Icon(Icons.games), label: AppLocalizations.of(context)!.navGuess), + BottomNavigationBarItem(icon: const Icon(Icons.settings), label: AppLocalizations.of(context)!.navSystem), ], ), ), diff --git a/lib/presentation/pages/pokemon_detail.dart b/lib/presentation/pages/pokemon_detail.dart index 4971709..9553c78 100644 --- a/lib/presentation/pages/pokemon_detail.dart +++ b/lib/presentation/pages/pokemon_detail.dart @@ -1,49 +1,80 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/pokemon.dart'; +import '../providers/locale_provider.dart'; +import '../providers/repository_provider.dart'; +import '../providers/theme_provider.dart'; import '../widgets/detail/pokemon_detail_top.dart'; import '../widgets/detail/pokemon_stats_panel.dart'; /// Fiche détaillée d'un Pokémon (reçu via les arguments de route). -class PokemonDetailPage extends StatefulWidget { +class PokemonDetailPage extends ConsumerStatefulWidget { const PokemonDetailPage({Key? key}) : super(key: key); @override - State createState() => _PokemonDetailPageState(); + ConsumerState createState() => _PokemonDetailPageState(); } -class _PokemonDetailPageState extends State { +class _PokemonDetailPageState extends ConsumerState { bool _isShiny = false; + String? _englishGenus; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _fetchEnglishGenusIfNeeded(); + } + + Future _fetchEnglishGenusIfNeeded() async { + final lang = ref.read(localeProvider).languageCode; + if (lang != 'en') return; + final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon; + final genus = await ref.read(pokemonRepositoryProvider).getEnglishGenus(pokemon.id); + if (mounted && genus != null) setState(() => _englishGenus = genus); + } @override Widget build(BuildContext context) { final pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon; + final palette = appPalettes[ref.watch(themeProvider)]; + final primaryDark = HSLColor.fromColor(palette.primary) + .withLightness( + (HSLColor.fromColor(palette.primary).lightness - 0.12).clamp(0.0, 1.0)) + .toColor(); return Scaffold( - backgroundColor: const Color(0xFF1B2333), + backgroundColor: palette.surface, body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), child: Container( decoration: BoxDecoration( - color: const Color(0xFFD32F2F), + color: palette.primary, borderRadius: BorderRadius.circular(30), - border: Border.all(color: const Color(0xFFA12020), width: 4), + border: Border.all(color: primaryDark, width: 4), ), child: SingleChildScrollView( child: Column( children: [ - _backBar(context), + _backBar(context, primaryDark), PokemonDetailTop( pokemon: pokemon, isShiny: _isShiny, onToggleShiny: () => setState(() => _isShiny = !_isShiny), + primaryColor: palette.primary, + primaryDark: primaryDark, + surfaceColor: palette.surface, ), const SizedBox(height: 20), - _hinge(), + _hinge(primaryDark), const SizedBox(height: 20), - PokemonStatsPanel(pokemon: pokemon), + PokemonStatsPanel( + pokemon: pokemon, + surfaceColor: palette.surface, + descriptionOverride: _englishGenus, + ), const SizedBox(height: 30), - _bottomDots(), + _bottomDots(primaryDark), const SizedBox(height: 20), ], ), @@ -54,7 +85,7 @@ class _PokemonDetailPageState extends State { ); } - Widget _backBar(BuildContext context) { + Widget _backBar(BuildContext context, Color primaryDark) { return Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 16), @@ -63,24 +94,24 @@ class _PokemonDetailPageState extends State { onTap: () => Navigator.pop(context), child: Container( padding: const EdgeInsets.all(4), - decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle), + decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle), child: const Icon(Icons.arrow_back, color: Colors.white), ), ), ); } - Widget _hinge() { + Widget _hinge(Color primaryDark) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Container(height: 6, width: 40, color: const Color(0xFFA12020)), - Container(height: 6, width: 40, color: const Color(0xFFA12020)), + Container(height: 6, width: 40, color: primaryDark), + Container(height: 6, width: 40, color: primaryDark), ], ); } - Widget _bottomDots() { + Widget _bottomDots(Color primaryDark) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( @@ -89,7 +120,7 @@ class _PokemonDetailPageState extends State { width: 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: const BoxDecoration(color: Color(0xFFA12020), shape: BoxShape.circle), + decoration: BoxDecoration(color: primaryDark, shape: BoxShape.circle), ), ), ); diff --git a/lib/presentation/pages/pokemon_list.dart b/lib/presentation/pages/pokemon_list.dart index 4732538..c23c24d 100644 --- a/lib/presentation/pages/pokemon_list.dart +++ b/lib/presentation/pages/pokemon_list.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/pokemon.dart'; import '../../l10n/app_localizations.dart'; import '../providers/pokedex_provider.dart'; +import '../providers/theme_provider.dart'; import '../widgets/pokemon_tile.dart'; import '../widgets/scanline_overlay.dart'; import '../widgets/list/pokedex_list_header.dart'; @@ -34,6 +35,7 @@ class _PokemonListPageState extends ConsumerState { @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; + final palette = appPalettes[ref.watch(themeProvider)]; final pokedexAsync = ref.watch(pokedexProvider); final caughtCount = ref.watch(caughtCountProvider); @@ -47,8 +49,8 @@ class _PokemonListPageState extends ConsumerState { height: 40, child: Row( children: [ - _buildTab('ALL', l.tabAll, _filter == 'ALL'), - _buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT'), + _buildTab('ALL', l.tabAll, _filter == 'ALL', palette.primary), + _buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT', palette.primary), ], ), ), @@ -79,7 +81,7 @@ class _PokemonListPageState extends ConsumerState { ), Container( height: 24, - color: const Color(0xFF1B2333), + color: palette.surface, alignment: Alignment.center, child: Text(l.pokedexFooter, style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)), @@ -105,7 +107,7 @@ class _PokemonListPageState extends ConsumerState { ); } - Widget _buildTab(String key, String label, bool isSelected) { + Widget _buildTab(String key, String label, bool isSelected, Color primaryColor) { return Expanded( child: GestureDetector( onTap: () { @@ -118,7 +120,7 @@ class _PokemonListPageState extends ConsumerState { decoration: BoxDecoration( color: isSelected ? const Color(0xFFB0BEC5) : Colors.transparent, border: isSelected - ? const Border(bottom: BorderSide(color: Color(0xFFD32F2F), width: 3)) + ? Border(bottom: BorderSide(color: primaryColor, width: 3)) : null, ), alignment: Alignment.center, diff --git a/lib/presentation/providers/game_provider.dart b/lib/presentation/providers/game_provider.dart index ce3a182..408d13e 100644 --- a/lib/presentation/providers/game_provider.dart +++ b/lib/presentation/providers/game_provider.dart @@ -35,7 +35,9 @@ class GameNotifier extends Notifier { final repo = ref.read(pokemonRepositoryProvider); final isShiny = _random.nextInt(AppConstants.shinyOdds) == 0; - final id = ref.read(genFilterProvider.notifier).randomId(_random); + final genFilter = ref.read(genFilterProvider.notifier); + await genFilter.ready; + final id = genFilter.randomId(_random); try { final pokemon = await repo.getById(id); if (pokemon == null) { diff --git a/lib/presentation/providers/gen_filter_provider.dart b/lib/presentation/providers/gen_filter_provider.dart index 474c903..2231497 100644 --- a/lib/presentation/providers/gen_filter_provider.dart +++ b/lib/presentation/providers/gen_filter_provider.dart @@ -6,10 +6,12 @@ import '../../core/config/app_constants.dart'; /// Gère les générations activées pour le tirage et fournit un id aléatoire dans celles-ci. /// La sélection est persistée dans les préférences. class GenFilterNotifier extends Notifier> { + late final Future _ready; + @override Set build() { - _load(); - // Default: all gens enabled + _ready = _load(); + // Default: all gens enabled — overwritten by _load() once prefs are read. return Set.from(List.generate(AppConstants.genRanges.length, (i) => i)); } @@ -21,6 +23,9 @@ class GenFilterNotifier extends Notifier> { } } + /// Resolves once the persisted filter has been loaded from SharedPreferences. + Future get ready => _ready; + Future toggle(int genIndex) async { final next = Set.from(state); if (next.contains(genIndex)) { diff --git a/lib/presentation/providers/pokedex_provider.dart b/lib/presentation/providers/pokedex_provider.dart index fdca4ca..8928960 100644 --- a/lib/presentation/providers/pokedex_provider.dart +++ b/lib/presentation/providers/pokedex_provider.dart @@ -5,7 +5,7 @@ import 'repository_provider.dart'; /// Liste complète du Pokédex (triée par id), avec synchro initiale gérée par le repository. class PokedexNotifier extends AsyncNotifier> { Future> _load() async { - final repo = ref.read(pokemonRepositoryProvider); + final repo = ref.watch(pokemonRepositoryProvider); final list = await repo.getAll(); list.sort((a, b) => a.id.compareTo(b.id)); return list; diff --git a/lib/presentation/providers/repository_provider.dart b/lib/presentation/providers/repository_provider.dart index 265aa73..541750d 100644 --- a/lib/presentation/providers/repository_provider.dart +++ b/lib/presentation/providers/repository_provider.dart @@ -4,10 +4,14 @@ import '../../data/datasources/pokemon_local_datasource.dart'; import '../../data/datasources/pokemon_remote_datasource.dart'; import '../../data/repositories/pokemon_repository_impl.dart'; import '../../domain/repositories/pokemon_repository.dart'; +import 'locale_provider.dart'; -/// Point d'injection unique du repository. Sur le web, pas de SQLite (local = null). +/// Point d'injection unique du repository. +/// Se reconstruit automatiquement quand la langue change, +/// ce qui invalide [pokedexProvider] en cascade. final pokemonRepositoryProvider = Provider((ref) { - final remote = PokemonRemoteDataSource(); - final local = kIsWeb ? null : PokemonLocalDataSource(); + final lang = ref.watch(localeProvider).languageCode; + final remote = PokemonRemoteDataSource(languageCode: lang); + final local = kIsWeb ? null : PokemonLocalDataSource(languageCode: lang); return PokemonRepositoryImpl(remote: remote, local: local); }); diff --git a/lib/presentation/providers/theme_provider.dart b/lib/presentation/providers/theme_provider.dart index 5502c6d..8d9c51d 100644 --- a/lib/presentation/providers/theme_provider.dart +++ b/lib/presentation/providers/theme_provider.dart @@ -2,25 +2,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -/// Palette de couleurs nommée de l'application (couleur primaire + surface de fond). +/// Palette de couleurs de l'application (couleur primaire + surface de fond). +/// Le nom affiché est géré via AppLocalizations (palette0…palette4). class AppPalette { - final String name; final Color primary; final Color surface; - const AppPalette({ - required this.name, - required this.primary, - required this.surface, - }); + const AppPalette({required this.primary, required this.surface}); } const List appPalettes = [ - AppPalette(name: 'Pokédex Rouge', primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)), - AppPalette(name: 'Océan Bleu', primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)), - AppPalette(name: 'Forêt Verte', primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)), - AppPalette(name: 'Foudre Jaune', primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)), - AppPalette(name: 'Ombre Violette',primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)), + AppPalette(primary: Color(0xFFD32F2F), surface: Color(0xFF1B2333)), + AppPalette(primary: Color(0xFF1565C0), surface: Color(0xFF0D1B2A)), + AppPalette(primary: Color(0xFF2E7D32), surface: Color(0xFF1A2B1A)), + AppPalette(primary: Color(0xFFF9A825), surface: Color(0xFF1C1A00)), + AppPalette(primary: Color(0xFF6A1B9A), surface: Color(0xFF1A0A2B)), ]; const String _prefsPaletteIndex = 'palette_index'; diff --git a/lib/presentation/theme/type_colors.dart b/lib/presentation/theme/type_colors.dart index 59e7d2c..b9f62a8 100644 --- a/lib/presentation/theme/type_colors.dart +++ b/lib/presentation/theme/type_colors.dart @@ -28,8 +28,37 @@ Color typeToColor(PokemonType type) { return map[type] ?? Colors.transparent; } -/// Nom du type avec une majuscule initiale. -String formatedTypeName(PokemonType type) { - final typeName = type.name; - return typeName[0].toUpperCase() + typeName.substring(1); +/// Nom du type localisé (EN par défaut, FR si languageCode == 'fr'). +String localizedTypeName(PokemonType type, String languageCode) { + if (languageCode == 'fr') { + const fr = { + PokemonType.normal: 'Normal', + PokemonType.fighting: 'Combat', + PokemonType.flying: 'Vol', + PokemonType.poison: 'Poison', + PokemonType.ground: 'Sol', + PokemonType.rock: 'Roche', + PokemonType.bug: 'Insecte', + PokemonType.ghost: 'Spectre', + PokemonType.steel: 'Acier', + PokemonType.fire: 'Feu', + PokemonType.water: 'Eau', + PokemonType.grass: 'Plante', + PokemonType.electric: 'Électrik', + PokemonType.psychic: 'Psy', + PokemonType.ice: 'Glace', + PokemonType.dragon: 'Dragon', + PokemonType.dark: 'Ténèbres', + PokemonType.fairy: 'Fée', + PokemonType.unknown: '?', + PokemonType.shadow: '?', + }; + return fr[type] ?? '?'; + } + // English: capitalise the enum name + final name = type.name; + return name[0].toUpperCase() + name.substring(1); } + +/// Nom du type avec une majuscule initiale (anglais, conservé pour compatibilité). +String formatedTypeName(PokemonType type) => localizedTypeName(type, 'en'); diff --git a/lib/presentation/widgets/detail/pokemon_detail_top.dart b/lib/presentation/widgets/detail/pokemon_detail_top.dart index b5e87e0..4367cb0 100644 --- a/lib/presentation/widgets/detail/pokemon_detail_top.dart +++ b/lib/presentation/widgets/detail/pokemon_detail_top.dart @@ -8,12 +8,18 @@ class PokemonDetailTop extends StatelessWidget { final Pokemon pokemon; final bool isShiny; final VoidCallback onToggleShiny; + final Color primaryColor; + final Color primaryDark; + final Color surfaceColor; const PokemonDetailTop({ super.key, required this.pokemon, required this.isShiny, required this.onToggleShiny, + required this.primaryColor, + required this.primaryDark, + required this.surfaceColor, }); @override @@ -21,9 +27,9 @@ class PokemonDetailTop extends StatelessWidget { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFF1B2333), + color: surfaceColor, borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFF1B2333), width: 8), + border: Border.all(color: surfaceColor, width: 8), ), child: Container( color: const Color(0xFF90A4AE), @@ -32,7 +38,7 @@ class PokemonDetailTop extends StatelessWidget { children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - color: const Color(0xFF1B2333), + color: surfaceColor, child: Text( "NO. ${pokemon.id.toString().padLeft(3, '0')}", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), @@ -60,7 +66,11 @@ class PokemonDetailTop extends StatelessWidget { Expanded( child: Text( pokemon.formatedName.toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 2), + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 2), overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/presentation/widgets/detail/pokemon_stats_panel.dart b/lib/presentation/widgets/detail/pokemon_stats_panel.dart index fa8573c..f7fecfc 100644 --- a/lib/presentation/widgets/detail/pokemon_stats_panel.dart +++ b/lib/presentation/widgets/detail/pokemon_stats_panel.dart @@ -1,19 +1,35 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../domain/entities/pokemon.dart'; import '../../../l10n/app_localizations.dart'; +import '../../providers/locale_provider.dart'; -/// Écran inférieur du détail : stats de base, description et éléments décoratifs. -class PokemonStatsPanel extends StatelessWidget { +/// Écran inférieur du détail : stats de base, description localisée et éléments décoratifs. +class PokemonStatsPanel extends ConsumerWidget { final Pokemon pokemon; - const PokemonStatsPanel({super.key, required this.pokemon}); + final Color surfaceColor; + final String? descriptionOverride; + + const PokemonStatsPanel({ + super.key, + required this.pokemon, + required this.surfaceColor, + this.descriptionOverride, + }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final l = AppLocalizations.of(context)!; + ref.watch(localeProvider); // watch locale so widget rebuilds on language change + final description = descriptionOverride ?? pokemon.description; + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: const Color(0xFF1B2333), borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration( + color: surfaceColor, + borderRadius: BorderRadius.circular(8), + ), child: Container( color: const Color(0xFFC8D1D8), padding: const EdgeInsets.all(16), @@ -23,12 +39,20 @@ class PokemonStatsPanel extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(l.baseStats, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), - Text("MODEL: DS-01", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold)), + Text(l.baseStats, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 2)), + Text("MODEL: DS-01", + style: TextStyle( + fontSize: 12, + color: Colors.grey[700], + fontWeight: FontWeight.bold)), ], ), const Divider(color: Colors.black38, thickness: 2, height: 20), - _StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)), + _StatBar(label: "HP", value: pokemon.hp, color: const Color(0xFFE53935)), _StatBar(label: "ATK", value: pokemon.atk, color: const Color(0xFFFB8C00)), _StatBar(label: "DEF", value: pokemon.def, color: const Color(0xFFFDD835)), _StatBar(label: "SPD", value: pokemon.spd, color: const Color(0xFF1E88E5)), @@ -36,16 +60,19 @@ class PokemonStatsPanel extends StatelessWidget { Container( width: double.infinity, padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: const Color(0xFFE2EBF0), border: Border.all(color: Colors.grey[400]!)), + decoration: BoxDecoration( + color: const Color(0xFFE2EBF0), + border: Border.all(color: Colors.grey[400]!), + ), child: Text( - pokemon.description != null && pokemon.description!.isNotEmpty - ? '"${pokemon.description!}"' + description != null && description.isNotEmpty + ? '"$description"' : '"${l.noDescription}"', style: const TextStyle(fontSize: 16, height: 1.5), ), ), const SizedBox(height: 16), - const _DecorativeLights(), + _DecorativeLights(accentColor: surfaceColor), ], ), ), @@ -53,7 +80,6 @@ class PokemonStatsPanel extends StatelessWidget { } } -/// Barre d'une statistique (libellé, jauge proportionnelle, valeur). class _StatBar extends StatelessWidget { final String label; final int value; @@ -62,20 +88,27 @@ class _StatBar extends StatelessWidget { @override Widget build(BuildContext context) { - final ratio = (value / 255).clamp(0.0, 1.0); // stat de base max supposée = 255 + final ratio = (value / 255).clamp(0.0, 1.0); return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ - SizedBox(width: 50, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), + 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()), + Expanded( + flex: (ratio * 100).toInt(), + child: Container(color: color)), + Expanded( + flex: 100 - (ratio * 100).toInt(), + child: Container()), ], ), ), @@ -84,7 +117,8 @@ class _StatBar extends StatelessWidget { SizedBox( width: 40, child: Text(value.toString(), - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), textAlign: TextAlign.right), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + textAlign: TextAlign.right), ), ], ), @@ -92,21 +126,26 @@ class _StatBar extends StatelessWidget { } } -/// Petites diodes décoratives en bas du panneau (purement cosmétiques). class _DecorativeLights extends StatelessWidget { - const _DecorativeLights(); + final Color accentColor; + const _DecorativeLights({required this.accentColor}); @override Widget build(BuildContext context) { + final dark = HSLColor.fromColor(accentColor) + .withLightness( + (HSLColor.fromColor(accentColor).lightness - 0.1).clamp(0.0, 1.0)) + .toColor(); + return Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( - color: const Color(0xFF1E88E5), + color: accentColor, shape: BoxShape.circle, - border: Border.all(color: const Color(0xFF1565C0), width: 2), + border: Border.all(color: dark, width: 2), ), ), const SizedBox(width: 8), @@ -122,9 +161,19 @@ class _DecorativeLights extends StatelessWidget { const Spacer(), Row( children: [ - Container(height: 6, width: 30, decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(3))), + 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))), + Container( + height: 6, + width: 30, + decoration: BoxDecoration( + color: Colors.grey[500], + borderRadius: BorderRadius.circular(3))), ], ), ], diff --git a/lib/presentation/widgets/game_over/game_over_actions.dart b/lib/presentation/widgets/game_over/game_over_actions.dart index f1db5dc..c8d876f 100644 --- a/lib/presentation/widgets/game_over/game_over_actions.dart +++ b/lib/presentation/widgets/game_over/game_over_actions.dart @@ -3,19 +3,27 @@ import '../../../l10n/app_localizations.dart'; /// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex. class GameOverActions extends StatelessWidget { + final Color primaryColor; + final Color primaryDark; final VoidCallback onTryAgain; final VoidCallback onBack; - const GameOverActions({super.key, required this.onTryAgain, required this.onBack}); + const GameOverActions({ + super.key, + required this.primaryColor, + required this.primaryDark, + required this.onTryAgain, + required this.onBack, + }); @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; return Column( children: [ - _button(label: l.tryAgain, icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain), + _button(label: l.tryAgain, icon: Icons.refresh, color: primaryColor, onPressed: onTryAgain), const SizedBox(height: 16), - _button(label: l.backToPokedex, icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack), + _button(label: l.backToPokedex, icon: Icons.menu_book, color: primaryDark, onPressed: onBack), ], ); } diff --git a/lib/presentation/widgets/game_over/game_over_header.dart b/lib/presentation/widgets/game_over/game_over_header.dart index 2174b0d..5dec6e9 100644 --- a/lib/presentation/widgets/game_over/game_over_header.dart +++ b/lib/presentation/widgets/game_over/game_over_header.dart @@ -6,17 +6,24 @@ import '../pokemon_image.dart'; class GameOverHeader extends StatelessWidget { final String pokemonImage; final String pokemonName; + final Color primaryDark; + final Color surfaceColor; - const GameOverHeader({super.key, required this.pokemonImage, required this.pokemonName}); + const GameOverHeader({ + super.key, + required this.pokemonImage, + required this.pokemonName, + required this.primaryDark, + required this.surfaceColor, + }); - static const _darkRed = Color(0xFF9E1B1B); static const _silverBg = Color(0xFFC8D1D8); @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; return Container( - decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)), + decoration: BoxDecoration(color: primaryDark, border: Border.all(color: primaryDark, width: 4)), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), @@ -24,7 +31,7 @@ class GameOverHeader extends StatelessWidget { child: Column( children: [ Container( - color: _darkRed, + color: primaryDark, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), child: Text( l.gameOver, @@ -44,10 +51,10 @@ class GameOverHeader extends StatelessWidget { Text( l.itWas(pokemonName), textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Color(0xFF1B2333), + color: surfaceColor, letterSpacing: 1, ), ), diff --git a/lib/presentation/widgets/game_over/hinge_divider.dart b/lib/presentation/widgets/game_over/hinge_divider.dart index 4a327de..a79d10e 100644 --- a/lib/presentation/widgets/game_over/hinge_divider.dart +++ b/lib/presentation/widgets/game_over/hinge_divider.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; /// Séparateur décoratif (deux traits + trois points) entre les blocs de l'écran game over. class HingeDivider extends StatelessWidget { - const HingeDivider({super.key}); - - static const _darkRed = Color(0xFF9E1B1B); + final Color color; + const HingeDivider({super.key, required this.color}); @override Widget build(BuildContext context) { @@ -12,7 +11,7 @@ class HingeDivider extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 24.0), child: Row( children: [ - Expanded(child: Container(height: 2, color: _darkRed)), + Expanded(child: Container(height: 2, color: color)), const SizedBox(width: 8), Row( mainAxisSize: MainAxisSize.min, @@ -22,12 +21,12 @@ class HingeDivider extends StatelessWidget { width: 8, height: 8, margin: const EdgeInsets.symmetric(horizontal: 4), - decoration: const BoxDecoration(color: _darkRed, shape: BoxShape.circle), + decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), ), ), const SizedBox(width: 8), - Expanded(child: Container(height: 2, color: _darkRed)), + Expanded(child: Container(height: 2, color: color)), ], ), ); diff --git a/lib/presentation/widgets/guess/gen_filter_section.dart b/lib/presentation/widgets/guess/gen_filter_section.dart index caa0122..38b1286 100644 --- a/lib/presentation/widgets/guess/gen_filter_section.dart +++ b/lib/presentation/widgets/guess/gen_filter_section.dart @@ -8,6 +8,7 @@ class GenFilterSection extends StatelessWidget { final VoidCallback onToggleOpen; final Set selectedGens; final void Function(int) onToggle; + final Color surfaceColor; const GenFilterSection({ super.key, @@ -15,6 +16,7 @@ class GenFilterSection extends StatelessWidget { required this.onToggleOpen, required this.selectedGens, required this.onToggle, + required this.surfaceColor, }); @override @@ -29,19 +31,19 @@ class GenFilterSection extends StatelessWidget { onPressed: onToggleOpen, icon: Icon( isOpen ? Icons.expand_less : Icons.filter_list, - color: const Color(0xFF1B2333), + color: surfaceColor, ), label: Text( AppLocalizations.of(context)!.genFilter, - style: const TextStyle( - color: Color(0xFF1B2333), + style: TextStyle( + color: surfaceColor, fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2, ), ), style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFF1B2333), width: 2), + side: BorderSide(color: surfaceColor, width: 2), padding: const EdgeInsets.symmetric(vertical: 12), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), ), @@ -51,7 +53,7 @@ class GenFilterSection extends StatelessWidget { duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: isOpen - ? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle) + ? _GenFilterPanel(selectedGens: selectedGens, onToggle: onToggle, surfaceColor: surfaceColor) : const SizedBox.shrink(), ), ], @@ -63,8 +65,9 @@ class GenFilterSection extends StatelessWidget { class _GenFilterPanel extends StatelessWidget { final Set selectedGens; final void Function(int) onToggle; + final Color surfaceColor; - const _GenFilterPanel({required this.selectedGens, required this.onToggle}); + const _GenFilterPanel({required this.selectedGens, required this.onToggle, required this.surfaceColor}); static const _genNames = [ 'Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX' @@ -76,7 +79,7 @@ class _GenFilterPanel extends StatelessWidget { margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: const Color(0xFF1B2333), width: 2), + border: Border.all(color: surfaceColor, width: 2), ), child: Column( children: List.generate(AppConstants.genRanges.length, (i) { diff --git a/lib/presentation/widgets/guess/guess_silhouette.dart b/lib/presentation/widgets/guess/guess_silhouette.dart index 3c7845e..71fb419 100644 --- a/lib/presentation/widgets/guess/guess_silhouette.dart +++ b/lib/presentation/widgets/guess/guess_silhouette.dart @@ -8,12 +8,14 @@ class GuessSilhouette extends StatelessWidget { final Pokemon pokemon; final bool isShiny; final bool isGuessed; + final Color surfaceColor; const GuessSilhouette({ super.key, required this.pokemon, required this.isShiny, required this.isGuessed, + required this.surfaceColor, }); @override @@ -27,7 +29,7 @@ class GuessSilhouette extends StatelessWidget { decoration: BoxDecoration( color: const Color(0xFF3B6EE3), borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF1B2333), width: 8), + border: Border.all(color: surfaceColor, width: 8), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -51,7 +53,7 @@ class GuessSilhouette extends StatelessWidget { ), ), Container( - color: const Color(0xFF1B2333), + color: surfaceColor, width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( diff --git a/lib/presentation/widgets/list/pokedex_list_header.dart b/lib/presentation/widgets/list/pokedex_list_header.dart index cfe82b6..f02a603 100644 --- a/lib/presentation/widgets/list/pokedex_list_header.dart +++ b/lib/presentation/widgets/list/pokedex_list_header.dart @@ -10,15 +10,9 @@ class PokedexListHeader extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), color: const Color(0xFF90A4AE), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Icon(Icons.menu, color: Colors.black87), - Text(AppLocalizations.of(context)!.listNational, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), - const Icon(Icons.search, color: Colors.black87), - ], - ), + alignment: Alignment.center, + child: Text(AppLocalizations.of(context)!.listNational, + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), ); } } diff --git a/lib/presentation/widgets/pokemon_type.dart b/lib/presentation/widgets/pokemon_type.dart index 63435f1..bda1e2d 100644 --- a/lib/presentation/widgets/pokemon_type.dart +++ b/lib/presentation/widgets/pokemon_type.dart @@ -10,7 +10,8 @@ class PokemonTypeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - String typeName = formatedTypeName(type); + final lang = Localizations.localeOf(context).languageCode; + String typeName = localizedTypeName(type, lang); Color typeColor = typeToColor(type); return Container( diff --git a/lib/presentation/widgets/system/language_picker.dart b/lib/presentation/widgets/system/language_picker.dart index 27fc205..b27400a 100644 --- a/lib/presentation/widgets/system/language_picker.dart +++ b/lib/presentation/widgets/system/language_picker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; import '../../providers/locale_provider.dart'; /// Sélecteur de langue de l'application (parmi [supportedLocales]). @@ -14,11 +15,13 @@ class LanguagePicker extends StatelessWidget { required this.onSelect, }); - static const _names = {'en': 'English', 'fr': 'Français'}; static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'}; @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + final names = {'en': l.langEnglish, 'fr': l.langFrench}; + return Column( children: supportedLocales.map((locale) { final isSelected = locale.languageCode == selected.languageCode; @@ -41,7 +44,7 @@ class LanguagePicker extends StatelessWidget { Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)), const SizedBox(width: 16), Text( - _names[locale.languageCode] ?? locale.languageCode, + names[locale.languageCode] ?? locale.languageCode, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/presentation/widgets/system/palette_picker.dart b/lib/presentation/widgets/system/palette_picker.dart index bf5d103..7e168db 100644 --- a/lib/presentation/widgets/system/palette_picker.dart +++ b/lib/presentation/widgets/system/palette_picker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; import '../../providers/theme_provider.dart'; /// Sélecteur de palette de couleurs de l'application. @@ -9,6 +10,9 @@ class PalettePicker extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + final paletteNames = [l.palette0, l.palette1, l.palette2, l.palette3, l.palette4]; + return Column( children: List.generate(appPalettes.length, (i) { final p = appPalettes[i]; @@ -53,7 +57,7 @@ class PalettePicker extends StatelessWidget { ), const SizedBox(width: 16), Text( - p.name, + paletteNames[i], style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600,