From 4eb488e28cd9a54481c977cc383136679bd873a4 Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Fri, 31 Oct 2025 13:40:14 +0100 Subject: [PATCH] themes --- lib/main.dart | 42 +-- lib/src/api/wk_client.dart | 22 +- lib/src/models/custom_kanji_item.dart | 27 +- lib/src/models/kanji_item.dart | 35 +- lib/src/models/theme_model.dart | 13 + lib/src/screens/add_card_screen.dart | 9 +- lib/src/screens/browse_screen.dart | 354 ++++++++++-------- .../screens/custom_card_details_screen.dart | 33 +- lib/src/screens/custom_quiz_screen.dart | 73 ++-- lib/src/screens/custom_srs_screen.dart | 14 +- lib/src/screens/home_screen.dart | 117 +++--- lib/src/screens/settings_screen.dart | 105 ++++-- lib/src/screens/vocab_screen.dart | 101 +++-- lib/src/services/deck_repository.dart | 6 +- lib/src/services/vocab_deck_repository.dart | 6 +- lib/src/themes.dart | 129 +++++++ 16 files changed, 691 insertions(+), 395 deletions(-) create mode 100644 lib/src/models/theme_model.dart create mode 100644 lib/src/themes.dart diff --git a/lib/main.dart b/lib/main.dart index a4eb04c..a8252b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:hirameki_srs/src/models/theme_model.dart'; import 'package:hirameki_srs/src/services/vocab_deck_repository.dart'; import 'package:provider/provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -9,16 +10,15 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); try { await dotenv.load(fileName: ".env"); - } catch (e) { - // It's okay if the .env file is not found. - // This is expected in release builds. - } + // No need to catch because the file is only needed for dev + } catch (_) {} runApp( MultiProvider( providers: [ Provider(create: (_) => DeckRepository()), Provider(create: (_) => VocabDeckRepository()), + ChangeNotifierProvider(create: (_) => ThemeModel()), ], child: const WkApp(), ), @@ -30,29 +30,15 @@ class WkApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Hirameki SRS', - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: const ColorScheme( - brightness: Brightness.dark, - primary: Color(0xFF90CAF9), // Light blue for primary elements - onPrimary: Colors.black, - secondary: Color(0xFFBBDEFB), // Slightly lighter blue for secondary elements - onSecondary: Colors.black, - tertiary: Color(0xFFA5D6A7), // Light green for success/correct states - onTertiary: Colors.black, - error: Color(0xFFEF9A9A), // Light red for error states - onError: Colors.black, - surface: Color(0xFF121212), // Very dark gray - onSurface: Colors.white, - surfaceContainer: Color(0xFF1E1E1E), // Slightly lighter dark gray - surfaceContainerHighest: Color(0xFF424242), // A distinct dark gray for surface variants - onSurfaceVariant: Colors.white70, - ), - useMaterial3: true, - ), - home: const StartScreen(), + return Consumer( + builder: (context, themeModel, child) { + return MaterialApp( + title: 'Hirameki SRS', + debugShowCheckedModeBanner: false, + theme: themeModel.currentTheme, + home: const StartScreen(), + ); + }, ); } -} \ No newline at end of file +} diff --git a/lib/src/api/wk_client.dart b/lib/src/api/wk_client.dart index 39adcd3..fddd46c 100644 --- a/lib/src/api/wk_client.dart +++ b/lib/src/api/wk_client.dart @@ -6,9 +6,16 @@ class WkClient { final Map headers; final String base = 'https://api.wanikani.com/v2'; - WkClient(this.apiKey) : headers = {'Authorization': 'Bearer $apiKey', 'Wanikani-Revision': '20170710', 'Accept': 'application/json'}; + WkClient(this.apiKey) + : headers = { + 'Authorization': 'Bearer $apiKey', + 'Wanikani-Revision': '20170710', + 'Accept': 'application/json', + }; - Future>> fetchAllAssignments({List? subjectTypes}) async { + Future>> fetchAllAssignments({ + List? subjectTypes, + }) async { final out = >[]; String url = '$base/assignments?page=1'; if (subjectTypes != null && subjectTypes.isNotEmpty) { @@ -30,13 +37,15 @@ class WkClient { return out; } - Future>> fetchAllSubjects({List? types}) async { + Future>> fetchAllSubjects({ + List? types, + }) async { final out = >[]; String url = '$base/subjects'; if (types != null && types.isNotEmpty) { url += '?types=${types.join(',')}'; } - + while (url.isNotEmpty) { final resp = await http.get(Uri.parse(url), headers: headers); if (resp.statusCode != 200) throw Exception('API ${resp.statusCode}'); @@ -56,7 +65,10 @@ class WkClient { final out = >[]; const batch = 100; for (var i = 0; i < ids.length; i += batch) { - final chunk = ids.sublist(i, i + batch > ids.length ? ids.length : i + batch); + final chunk = ids.sublist( + i, + i + batch > ids.length ? ids.length : i + batch, + ); String url = '$base/subjects?ids=${chunk.join(',')}&page=1'; while (true) { final resp = await http.get(Uri.parse(url), headers: headers); diff --git a/lib/src/models/custom_kanji_item.dart b/lib/src/models/custom_kanji_item.dart index c045761..16df01a 100644 --- a/lib/src/models/custom_kanji_item.dart +++ b/lib/src/models/custom_kanji_item.dart @@ -1,4 +1,3 @@ - class CustomKanjiItem { final String characters; final String meaning; @@ -25,7 +24,9 @@ class CustomKanjiItem { srsData.listeningComprehensionNextReview ??= oldNextReview; } } else { - DateTime? nextReview = json['nextReview'] != null ? DateTime.parse(json['nextReview'] as String) : null; + DateTime? nextReview = json['nextReview'] != null + ? DateTime.parse(json['nextReview'] as String) + : null; srsData = SrsData( japaneseToEnglish: json['srsLevel'] as int? ?? 0, japaneseToEnglishNextReview: nextReview, @@ -76,22 +77,32 @@ class SrsData { factory SrsData.fromJson(Map json) { return SrsData( japaneseToEnglish: json['japaneseToEnglish'] as int? ?? 0, - japaneseToEnglishNextReview: json['japaneseToEnglishNextReview'] != null ? DateTime.parse(json['japaneseToEnglishNextReview'] as String) : null, + japaneseToEnglishNextReview: json['japaneseToEnglishNextReview'] != null + ? DateTime.parse(json['japaneseToEnglishNextReview'] as String) + : null, englishToJapanese: json['englishToJapanese'] as int? ?? 0, - englishToJapaneseNextReview: json['englishToJapaneseNextReview'] != null ? DateTime.parse(json['englishToJapaneseNextReview'] as String) : null, + englishToJapaneseNextReview: json['englishToJapaneseNextReview'] != null + ? DateTime.parse(json['englishToJapaneseNextReview'] as String) + : null, listeningComprehension: json['listeningComprehension'] as int? ?? 0, - listeningComprehensionNextReview: json['listeningComprehensionNextReview'] != null ? DateTime.parse(json['listeningComprehensionNextReview'] as String) : null, + listeningComprehensionNextReview: + json['listeningComprehensionNextReview'] != null + ? DateTime.parse(json['listeningComprehensionNextReview'] as String) + : null, ); } Map toJson() { return { 'japaneseToEnglish': japaneseToEnglish, - 'japaneseToEnglishNextReview': japaneseToEnglishNextReview?.toIso8601String(), + 'japaneseToEnglishNextReview': japaneseToEnglishNextReview + ?.toIso8601String(), 'englishToJapanese': englishToJapanese, - 'englishToJapaneseNextReview': englishToJapaneseNextReview?.toIso8601String(), + 'englishToJapaneseNextReview': englishToJapaneseNextReview + ?.toIso8601String(), 'listeningComprehension': listeningComprehension, - 'listeningComprehensionNextReview': listeningComprehensionNextReview?.toIso8601String(), + 'listeningComprehensionNextReview': listeningComprehensionNextReview + ?.toIso8601String(), }; } } diff --git a/lib/src/models/kanji_item.dart b/lib/src/models/kanji_item.dart index dcbbbb9..f7e028b 100644 --- a/lib/src/models/kanji_item.dart +++ b/lib/src/models/kanji_item.dart @@ -3,7 +3,7 @@ enum QuizMode { kanjiToEnglish, englishToKanji, reading } class SrsItem { final int kanjiId; final QuizMode quizMode; - final String? readingType; // 'onyomi' or 'kunyomi' + final String? readingType; int srsStage; DateTime lastAsked; @@ -116,13 +116,14 @@ class VocabularyItem { final List pronunciationAudios; final Map srsItems = {}; - VocabularyItem( - {required this.id, - required this.level, - required this.characters, - required this.meanings, - required this.readings, - required this.pronunciationAudios}); + VocabularyItem({ + required this.id, + required this.level, + required this.characters, + required this.meanings, + required this.readings, + required this.pronunciationAudios, + }); factory VocabularyItem.fromSubject(Map subj) { final int id = subj['id'] as int; @@ -152,20 +153,18 @@ class VocabularyItem { final gender = metadata?['gender'] as String?; if (url != null && gender != null) { - pronunciationAudios.add(PronunciationAudio( - url: url, - gender: gender, - )); + pronunciationAudios.add(PronunciationAudio(url: url, gender: gender)); } } } return VocabularyItem( - id: id, - level: level, - characters: characters, - meanings: meanings, - readings: readings, - pronunciationAudios: pronunciationAudios); + id: id, + level: level, + characters: characters, + meanings: meanings, + readings: readings, + pronunciationAudios: pronunciationAudios, + ); } } diff --git a/lib/src/models/theme_model.dart b/lib/src/models/theme_model.dart new file mode 100644 index 0000000..a017945 --- /dev/null +++ b/lib/src/models/theme_model.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:hirameki_srs/src/themes.dart'; + +class ThemeModel extends ChangeNotifier { + ThemeData _currentTheme = Themes.dark; + + ThemeData get currentTheme => _currentTheme; + + void setTheme(ThemeData theme) { + _currentTheme = theme; + notifyListeners(); + } +} diff --git a/lib/src/screens/add_card_screen.dart b/lib/src/screens/add_card_screen.dart index 1969a79..ca74757 100644 --- a/lib/src/screens/add_card_screen.dart +++ b/lib/src/screens/add_card_screen.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:kana_kit/kana_kit.dart'; import '../models/custom_kanji_item.dart'; @@ -61,7 +60,9 @@ class _AddCardScreenState extends State { final newItem = CustomKanjiItem( characters: _japaneseController.text, meaning: _englishController.text, - kanji: _kanjiController.text.trim().isNotEmpty ? _kanjiController.text.trim() : null, + kanji: _kanjiController.text.trim().isNotEmpty + ? _kanjiController.text.trim() + : null, useInterval: _useInterval, srsData: srsData, ); @@ -73,9 +74,7 @@ class _AddCardScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Add New Card'), - ), + appBar: AppBar(title: const Text('Add New Card')), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( diff --git a/lib/src/screens/browse_screen.dart b/lib/src/screens/browse_screen.dart index 823de0f..c9c8528 100644 --- a/lib/src/screens/browse_screen.dart +++ b/lib/src/screens/browse_screen.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:hirameki_srs/src/themes.dart'; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import '../models/kanji_item.dart'; @@ -18,7 +19,8 @@ class BrowseScreen extends StatefulWidget { State createState() => _BrowseScreenState(); } -class _BrowseScreenState extends State with SingleTickerProviderStateMixin { +class _BrowseScreenState extends State + with SingleTickerProviderStateMixin { late TabController _tabController; late PageController _kanjiPageController; late PageController _vocabPageController; @@ -50,7 +52,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt _vocabPageController = PageController(); _tabController.addListener(() { - setState(() {}); // Rebuild to update the level selector + setState(() {}); }); _kanjiPageController.addListener(() { @@ -94,13 +96,17 @@ class _BrowseScreenState extends State with SingleTickerProviderSt child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('WaniKani API key is not set.', style: TextStyle(color: Colors.white)), + Text( + 'WaniKani API key is not set.', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () async { await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); + if (!mounted) return; _loadDecks(); }, child: const Text('Go to Settings'), @@ -115,9 +121,9 @@ class _BrowseScreenState extends State with SingleTickerProviderSt child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const CircularProgressIndicator(color: Colors.blueAccent), + CircularProgressIndicator(color: Theme.of(context).colorScheme.primary), const SizedBox(height: 16), - Text(_status, style: const TextStyle(color: Colors.white)), + Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), ], ), ); @@ -128,22 +134,28 @@ class _BrowseScreenState extends State with SingleTickerProviderSt Widget _buildCustomSrsTab() { if (_customDeck.isEmpty) { - return const Center( - child: Text('No custom cards yet.', style: TextStyle(color: Colors.white)), + return Center( + child: Text( + 'No custom cards yet.', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ); } return _buildCustomGridView(_customDeck); } Widget _buildPaginatedView( - Map> groupedItems, - List sortedLevels, - PageController pageController, - Widget Function(List) buildPageContent) { + Map> groupedItems, + List sortedLevels, + PageController pageController, + Widget Function(List) buildPageContent, + ) { if (sortedLevels.isEmpty) { - return const Center( - child: - Text('No items to display.', style: TextStyle(color: Colors.white)), + return Center( + child: Text( + 'No items to display.', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ); } @@ -160,10 +172,11 @@ class _BrowseScreenState extends State with SingleTickerProviderSt padding: const EdgeInsets.all(16.0), child: Text( 'Level $level', - style: const TextStyle( - fontSize: 24, - color: Colors.white, - fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), ), ), Expanded(child: buildPageContent(levelItems)), @@ -183,7 +196,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt return Container( padding: const EdgeInsets.symmetric(vertical: 8.0), - color: const Color(0xFF1F1F1F), + color: Theme.of(context).colorScheme.surfaceContainer, height: 60, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -196,11 +209,17 @@ class _BrowseScreenState extends State with SingleTickerProviderSt padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton( onPressed: () { - controller.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + controller.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); }, style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? Colors.blueAccent : const Color(0xFF333333), - foregroundColor: Colors.white, + backgroundColor: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: Theme.of(context).colorScheme.onPrimary, shape: const CircleBorder(), padding: const EdgeInsets.all(12), ), @@ -266,7 +285,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt return GestureDetector( onTap: () => _showVocabDetailsDialog(context, item), child: Card( - color: const Color(0xFF1E1E1E), + color: Theme.of(context).colorScheme.surfaceContainer, margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Padding( padding: const EdgeInsets.all(12.0), @@ -275,7 +294,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt Expanded( child: Text( item.characters, - style: const TextStyle(fontSize: 24, color: Colors.white), + style: TextStyle(fontSize: 24, color: Theme.of(context).colorScheme.onSurface), ), ), const SizedBox(width: 16), @@ -286,7 +305,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt children: [ Text( item.meanings.join(', '), - style: const TextStyle(color: Colors.grey), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), @@ -327,7 +346,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt } return Card( - color: const Color(0xFF1E1E1E), + color: Theme.of(context).colorScheme.surfaceContainer, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -335,7 +354,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt children: [ Text( item.characters, - style: const TextStyle(fontSize: 32, color: Colors.white), + style: TextStyle(fontSize: 32, color: Theme.of(context).colorScheme.onSurface), textAlign: TextAlign.center, ), const SizedBox(height: 8), @@ -354,8 +373,8 @@ class _BrowseScreenState extends State with SingleTickerProviderSt child: SizedBox( height: 10, child: LinearProgressIndicator( - value: level / 9.0, // Max SRS level is 9 - backgroundColor: Colors.grey[800], + value: level / 9.0, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( _getColorForSrsLevel(level), ), @@ -366,13 +385,17 @@ class _BrowseScreenState extends State with SingleTickerProviderSt } Color _getColorForSrsLevel(int level) { - if (level >= 9) return Colors.purple; - if (level >= 8) return Colors.blue; - if (level >= 7) return Colors.lightBlue; - if (level >= 5) return Colors.green; - if (level >= 3) return Colors.yellow; - if (level >= 1) return Colors.orange; - return Colors.red; + final srsColors = Theme.of(context).srsColors; + if (level >= 9) return srsColors.level9; + if (level >= 8) return srsColors.level8; + if (level >= 7) return srsColors.level7; + if (level >= 6) return srsColors.level6; + if (level >= 5) return srsColors.level5; + if (level >= 4) return srsColors.level4; + if (level >= 3) return srsColors.level3; + if (level >= 2) return srsColors.level2; + if (level >= 1) return srsColors.level1; + return Colors.grey; } void _showReadingsDialog(KanjiItem kanji) { @@ -404,12 +427,12 @@ class _BrowseScreenState extends State with SingleTickerProviderSt showDialog( context: context, - builder: (context) { + builder: (dialogContext) { return AlertDialog( - backgroundColor: const Color(0xFF1E1E1E), + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, title: Text( 'Details for ${kanji.characters}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), content: SingleChildScrollView( child: Column( @@ -418,50 +441,56 @@ class _BrowseScreenState extends State with SingleTickerProviderSt children: [ Text( 'Level: ${kanji.level}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), if (kanji.meanings.isNotEmpty) Text( 'Meanings: ${kanji.meanings.join(', ')}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), if (kanji.onyomi.isNotEmpty) Text( 'On\'yomi: ${kanji.onyomi.join(', ')}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), if (kanji.kunyomi.isNotEmpty) Text( 'Kun\'yomi: ${kanji.kunyomi.join(', ')}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty) - const Text( + Text( 'No readings available.', - style: TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), - const Divider(color: Colors.grey), + Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), - const Text( + Text( 'SRS Scores:', style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold), + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + ...srsScores.entries.map( + (entry) => Text( + ' ${entry.key}: ${entry.value}', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ), - ...srsScores.entries.map((entry) => Text( - ' ${entry.key}: ${entry.value}', - style: const TextStyle(color: Colors.white), - )), ], ), ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close', - style: TextStyle(color: Colors.blueAccent)), + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text( + 'Close', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), ), ], ); @@ -473,8 +502,10 @@ class _BrowseScreenState extends State with SingleTickerProviderSt setState(() => _loading = true); try { final kanjiRepo = Provider.of(context, listen: false); - final vocabRepo = - Provider.of(context, listen: false); + final vocabRepo = Provider.of( + context, + listen: false, + ); await kanjiRepo.loadApiKey(); final apiKey = kanjiRepo.apiKey; @@ -537,9 +568,9 @@ class _BrowseScreenState extends State with SingleTickerProviderSt @override Widget build(BuildContext context) { return Scaffold( - appBar: - _isSelectionMode ? _buildSelectionAppBar() : _buildDefaultAppBar(), - backgroundColor: const Color(0xFF121212), + appBar: _isSelectionMode + ? _buildSelectionAppBar() + : _buildDefaultAppBar(), body: Column( children: [ Expanded( @@ -548,18 +579,19 @@ class _BrowseScreenState extends State with SingleTickerProviderSt children: [ _buildWaniKaniTab( _buildPaginatedView( - _kanjiByLevel, - _kanjiSortedLevels, - _kanjiPageController, - (items) => _buildGridView(items.cast())), + _kanjiByLevel, + _kanjiSortedLevels, + _kanjiPageController, + (items) => _buildGridView(items.cast()), + ), ), _buildWaniKaniTab( _buildPaginatedView( - _vocabByLevel, - _vocabSortedLevels, - _vocabPageController, - (items) => - _buildListView(items.cast())), + _vocabByLevel, + _vocabSortedLevels, + _vocabPageController, + (items) => _buildListView(items.cast()), + ), ), _buildCustomSrsTab(), ], @@ -577,9 +609,10 @@ class _BrowseScreenState extends State with SingleTickerProviderSt floatingActionButton: _tabController.index == 2 ? FloatingActionButton( onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => AddCardScreen()), - ); + await Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => AddCardScreen())); + if (!mounted) return; _loadCustomDeck(); }, child: const Icon(Icons.add), @@ -615,14 +648,8 @@ class _BrowseScreenState extends State with SingleTickerProviderSt ), title: Text('${_selectedItems.length} selected'), actions: [ - IconButton( - icon: const Icon(Icons.select_all), - onPressed: _selectAll, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: _deleteSelected, - ), + IconButton(icon: const Icon(Icons.select_all), onPressed: _selectAll), + IconButton(icon: const Icon(Icons.delete), onPressed: _deleteSelected), IconButton( icon: Icon(_toggleIntervalIcon), onPressed: _toggleIntervalForSelected, @@ -652,28 +679,30 @@ class _BrowseScreenState extends State with SingleTickerProviderSt void _deleteSelected() { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: const Text('Delete Selected'), - content: - Text('Are you sure you want to delete ${_selectedItems.length} cards?'), + content: Text( + 'Are you sure you want to delete ${_selectedItems.length} cards?', + ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), TextButton( - onPressed: () async { - final navigator = Navigator.of(context); - for (final item in _selectedItems) { - await _customDeckRepository.deleteCard(item); - } - setState(() { - _isSelectionMode = false; - _selectedItems.clear(); - }); - await _loadCustomDeck(); - if (!mounted) return; - navigator.pop(); + onPressed: () { + Navigator.of(dialogContext).pop(); + () async { + for (final item in _selectedItems) { + await _customDeckRepository.deleteCard(item); + } + if (!mounted) return; + setState(() { + _isSelectionMode = false; + _selectedItems.clear(); + }); + _loadCustomDeck(); + }(); }, child: const Text('Delete'), ), @@ -688,8 +717,9 @@ class _BrowseScreenState extends State with SingleTickerProviderSt } final bool targetState = _selectedItems.any((item) => !item.useInterval); - final selectedCharacters = - _selectedItems.map((item) => item.characters).toSet(); + final selectedCharacters = _selectedItems + .map((item) => item.characters) + .toSet(); final List updatedItems = []; for (final item in _selectedItems) { @@ -765,13 +795,13 @@ class _BrowseScreenState extends State with SingleTickerProviderSt child: Card( shape: RoundedRectangleBorder( side: isSelected - ? const BorderSide(color: Colors.blue, width: 2.0) + ? BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0) : BorderSide.none, borderRadius: BorderRadius.circular(12.0), ), color: isSelected - ? Colors.blue.withAlpha((255 * 0.5).round()) - : const Color(0xFF1E1E1E), + ? Theme.of(context).colorScheme.primary.withAlpha((255 * 0.5).round()) + : Theme.of(context).colorScheme.surfaceContainer, child: Stack( children: [ Padding( @@ -785,27 +815,34 @@ class _BrowseScreenState extends State with SingleTickerProviderSt item.kanji?.isNotEmpty == true ? item.kanji! : item.characters, - style: - const TextStyle(fontSize: 32, color: Colors.white), + style: TextStyle( + fontSize: 32, + color: Theme.of(context).colorScheme.onSurface, + ), textAlign: TextAlign.center, ), ), const SizedBox(height: 8), Text( item.meaning, - style: - const TextStyle(color: Colors.grey, fontSize: 16), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16, + ), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), - Builder(builder: (context) { - final avgSrs = (item.srsData.japaneseToEnglish + - item.srsData.englishToJapanese + - item.srsData.listeningComprehension) / - 3; - return _buildSrsIndicator(avgSrs.round()); - }), + Builder( + builder: (context) { + final avgSrs = + (item.srsData.japaneseToEnglish + + item.srsData.englishToJapanese + + item.srsData.listeningComprehension) / + 3; + return _buildSrsIndicator(avgSrs.round()); + }, + ), ], ), ), @@ -813,11 +850,7 @@ class _BrowseScreenState extends State with SingleTickerProviderSt Positioned( top: 4, right: 4, - child: Icon( - Icons.timer, - color: Colors.green, - size: 16, - ), + child: Icon(Icons.timer, color: Theme.of(context).colorScheme.tertiary, size: 16), ), ], ), @@ -848,9 +881,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { } Future _fetchExampleSentences() async { + final theme = Theme.of(context); try { final uri = Uri.parse( - 'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}'); + 'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}', + ); final response = await http.get(uri); if (response.statusCode == 200) { final data = jsonDecode(utf8.decode(response.bodyBytes)); @@ -861,15 +896,24 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { (result['japanese'] as List).isNotEmpty && result['senses'] != null && (result['senses'] as List).isNotEmpty) { - final japaneseWord = result['japanese'][0]['word'] ?? result['japanese'][0]['reading']; - final englishDefinition = result['senses'][0]['english_definitions'].join(', '); + final japaneseWord = + result['japanese'][0]['word'] ?? + result['japanese'][0]['reading']; + final englishDefinition = + result['senses'][0]['english_definitions'].join(', '); if (japaneseWord != null && englishDefinition != null) { sentences.add( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(japaneseWord, style: const TextStyle(color: Colors.white)), - Text(englishDefinition, style: const TextStyle(color: Colors.grey)), + Text( + japaneseWord, + style: TextStyle(color: theme.colorScheme.onSurface), + ), + Text( + englishDefinition, + style: TextStyle(color: theme.colorScheme.onSurfaceVariant), + ), const SizedBox(height: 8), ], ), @@ -879,7 +923,12 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { } } if (sentences.isEmpty) { - sentences.add(const Text('No example sentences found.', style: TextStyle(color: Colors.white))); + sentences.add( + Text( + 'No example sentences found.', + style: TextStyle(color: theme.colorScheme.onSurface), + ), + ); } if (mounted) { setState(() { @@ -890,7 +939,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { if (mounted) { setState(() { _exampleSentences = [ - const Text('Failed to load example sentences.', style: TextStyle(color: Colors.red)) + Text( + 'Failed to load example sentences.', + style: TextStyle(color: theme.colorScheme.error), + ), ]; }); } @@ -899,7 +951,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { if (mounted) { setState(() { _exampleSentences = [ - const Text('Error loading example sentences.', style: TextStyle(color: Colors.red)) + Text( + 'Error loading example sentences.', + style: TextStyle(color: theme.colorScheme.error), + ), ]; }); } @@ -908,11 +963,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { @override Widget build(BuildContext context) { - final srsScores = { - 'JP -> EN': 0, - 'EN -> JP': 0, - 'Audio': 0, - }; + final srsScores = {'JP -> EN': 0, 'EN -> JP': 0, 'Audio': 0}; for (final entry in widget.vocab.srsItems.entries) { final srsItem = entry.value; @@ -936,37 +987,39 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { children: [ Text( 'Level: ${widget.vocab.level}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), if (widget.vocab.meanings.isNotEmpty) Text( 'Meanings: ${widget.vocab.meanings.join(', ')}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), if (widget.vocab.readings.isNotEmpty) Text( 'Readings: ${widget.vocab.readings.join(', ')}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 16), - const Divider(color: Colors.grey), + Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), - const Text( + Text( 'SRS Scores:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), + ), + ...srsScores.entries.map( + (entry) => Text( + ' ${entry.key}: ${entry.value}', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ), - ...srsScores.entries.map((entry) => Text( - ' ${entry.key}: ${entry.value}', - style: const TextStyle(color: Colors.white), - )), const SizedBox(height: 16), - const Divider(color: Colors.grey), + Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 16), - const Text( + Text( 'Example Sentences:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), ), ..._exampleSentences, ], @@ -978,21 +1031,24 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) { showDialog( context: context, - builder: (context) { + builder: (dialogContext) { return AlertDialog( - backgroundColor: const Color(0xFF1E1E1E), + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, title: Text( 'Details for ${vocab.characters}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), content: _VocabDetailsDialog(vocab: vocab), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close', style: TextStyle(color: Colors.blueAccent)), + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text( + 'Close', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), ), ], ); }, ); -} \ No newline at end of file +} diff --git a/lib/src/screens/custom_card_details_screen.dart b/lib/src/screens/custom_card_details_screen.dart index 6c3f2f3..cf98ff7 100644 --- a/lib/src/screens/custom_card_details_screen.dart +++ b/lib/src/screens/custom_card_details_screen.dart @@ -6,8 +6,11 @@ class CustomCardDetailsScreen extends StatefulWidget { final CustomKanjiItem item; final CustomDeckRepository repository; - const CustomCardDetailsScreen( - {super.key, required this.item, required this.repository}); + const CustomCardDetailsScreen({ + super.key, + required this.item, + required this.repository, + }); @override State createState() => @@ -41,7 +44,9 @@ class _CustomCardDetailsScreenState extends State { final updatedItem = CustomKanjiItem( characters: _japaneseController.text, meaning: _englishController.text, - kanji: _kanjiController.text.trim().isNotEmpty ? _kanjiController.text.trim() : null, + kanji: _kanjiController.text.trim().isNotEmpty + ? _kanjiController.text.trim() + : null, useInterval: _useInterval, srsData: widget.item.srsData, ); @@ -79,10 +84,7 @@ class _CustomCardDetailsScreenState extends State { appBar: AppBar( title: const Text('Edit Card'), actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: _deleteCard, - ), + IconButton(icon: const Icon(Icons.delete), onPressed: _deleteCard), ], ), body: Padding( @@ -111,10 +113,19 @@ class _CustomCardDetailsScreenState extends State { }, ), const SizedBox(height: 20), - const Text('SRS Levels', style: TextStyle(fontWeight: FontWeight.bold)), - Text('Jpn→Eng: ${widget.item.srsData.japaneseToEnglish} (Next review: ${widget.item.srsData.japaneseToEnglishNextReview?.toString() ?? 'N/A'})'), - Text('Eng→Jpn: ${widget.item.srsData.englishToJapanese} (Next review: ${widget.item.srsData.englishToJapaneseNextReview?.toString() ?? 'N/A'})'), - Text('Listening: ${widget.item.srsData.listeningComprehension} (Next review: ${widget.item.srsData.listeningComprehensionNextReview?.toString() ?? 'N/A'})'), + const Text( + 'SRS Levels', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + 'Jpn→Eng: ${widget.item.srsData.japaneseToEnglish} (Next review: ${widget.item.srsData.japaneseToEnglishNextReview?.toString() ?? 'N/A'})', + ), + Text( + 'Eng→Jpn: ${widget.item.srsData.englishToJapanese} (Next review: ${widget.item.srsData.englishToJapaneseNextReview?.toString() ?? 'N/A'})', + ), + Text( + 'Listening: ${widget.item.srsData.listeningComprehension} (Next review: ${widget.item.srsData.listeningComprehensionNextReview?.toString() ?? 'N/A'})', + ), const SizedBox(height: 20), ElevatedButton( onPressed: _saveChanges, diff --git a/lib/src/screens/custom_quiz_screen.dart b/lib/src/screens/custom_quiz_screen.dart index aaf8040..2dcc1ab 100644 --- a/lib/src/screens/custom_quiz_screen.dart +++ b/lib/src/screens/custom_quiz_screen.dart @@ -5,7 +5,11 @@ import '../models/custom_kanji_item.dart'; import '../widgets/options_grid.dart'; import '../widgets/kanji_card.dart'; -enum CustomQuizMode { japaneseToEnglish, englishToJapanese, listeningComprehension } +enum CustomQuizMode { + japaneseToEnglish, + englishToJapanese, + listeningComprehension, +} class CustomQuizScreen extends StatefulWidget { final List deck; @@ -53,10 +57,7 @@ class CustomQuizScreenState extends State vsync: this, ); _shakeAnimation = Tween(begin: 0, end: 1).animate( - CurvedAnimation( - parent: _shakeController, - curve: Curves.elasticIn, - ), + CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn), ); } @@ -82,7 +83,8 @@ class CustomQuizScreenState extends State } void playAudio() { - if (widget.quizMode == CustomQuizMode.listeningComprehension && _currentIndex < _shuffledDeck.length) { + if (widget.quizMode == CustomQuizMode.listeningComprehension && + _currentIndex < _shuffledDeck.length) { _speak(_shuffledDeck[_currentIndex].characters); } } @@ -101,20 +103,30 @@ class CustomQuizScreenState extends State void _generateOptions() { final currentItem = _shuffledDeck[_currentIndex]; - if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) { + if (widget.quizMode == CustomQuizMode.listeningComprehension || + widget.quizMode == CustomQuizMode.japaneseToEnglish) { _options = [currentItem.meaning]; } else { - _options = [widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters]; + _options = [ + widget.useKanji && currentItem.kanji != null + ? currentItem.kanji! + : currentItem.characters, + ]; } final otherItems = widget.deck .where((item) => item.characters != currentItem.characters) .toList(); otherItems.shuffle(); for (var i = 0; i < min(3, otherItems.length); i++) { - if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) { + if (widget.quizMode == CustomQuizMode.listeningComprehension || + widget.quizMode == CustomQuizMode.japaneseToEnglish) { _options.add(otherItems[i].meaning); } else { - _options.add(widget.useKanji && otherItems[i].kanji != null ? otherItems[i].kanji! : otherItems[i].characters); + _options.add( + widget.useKanji && otherItems[i].kanji != null + ? otherItems[i].kanji! + : otherItems[i].characters, + ); } } _options.shuffle(); @@ -123,7 +135,9 @@ class CustomQuizScreenState extends State void _checkAnswer(String answer) async { final currentItem = _shuffledDeck[_currentIndex]; final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese) - ? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters) + ? (widget.useKanji && currentItem.kanji != null + ? currentItem.kanji! + : currentItem.characters) : currentItem.meaning; final isCorrect = answer == correctAnswer; @@ -157,7 +171,8 @@ class CustomQuizScreenState extends State currentItem.srsData.englishToJapaneseNextReview = newNextReview; break; case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = newNextReview; + currentItem.srsData.listeningComprehensionNextReview = + newNextReview; break; } } else { @@ -174,7 +189,8 @@ class CustomQuizScreenState extends State currentItem.srsData.englishToJapaneseNextReview = newNextReview; break; case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = newNextReview; + currentItem.srsData.listeningComprehensionNextReview = + newNextReview; break; } } @@ -194,35 +210,35 @@ class CustomQuizScreenState extends State widget.onCardReviewed(currentItem); } - // --- SnackBar Logic (new) --- final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese) - ? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters) + ? (widget.useKanji && currentItem.kanji != null + ? currentItem.kanji! + : currentItem.characters) : currentItem.meaning; final snack = SnackBar( content: Text( isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', style: TextStyle( - color: isCorrect ? Colors.greenAccent : Colors.redAccent, + color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold, ), ), - backgroundColor: const Color(0xFF222222), + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 900), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(snack); } - // --- End SnackBar Logic --- if (isCorrect) { if (widget.quizMode == CustomQuizMode.japaneseToEnglish) { await _speak(currentItem.characters); } - await Future.delayed(const Duration(milliseconds: 500)); // Small delay after correct answer + await Future.delayed(const Duration(milliseconds: 500)); } else { _shakeController.forward(from: 0); - await Future.delayed(const Duration(milliseconds: 900)); // Delay for shake animation + await Future.delayed(const Duration(milliseconds: 900)); } _nextQuestion(); @@ -255,15 +271,15 @@ class CustomQuizScreenState extends State @override Widget build(BuildContext context) { if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) { - return const Center( - child: Text('Review session complete!'), - ); + return Center(child: Text('Review session complete!', style: TextStyle(color: Theme.of(context).colorScheme.onSurface))); } final currentItem = _shuffledDeck[_currentIndex]; final question = (widget.quizMode == CustomQuizMode.englishToJapanese) ? currentItem.meaning - : (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters); + : (widget.useKanji && currentItem.kanji != null + ? currentItem.kanji! + : currentItem.characters); Widget promptWidget; if (widget.quizMode == CustomQuizMode.listeningComprehension) { @@ -276,7 +292,7 @@ class CustomQuizScreenState extends State onTap: () => _speak(question), child: Text( question, - style: const TextStyle(fontSize: 48), + style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface), textAlign: TextAlign.center, ), ); @@ -296,10 +312,7 @@ class CustomQuizScreenState extends State maxWidth: 500, minHeight: 150, ), - child: KanjiCard( - characterWidget: promptWidget, - subtitle: '', - ), + child: KanjiCard(characterWidget: promptWidget, subtitle: ''), ), ), ), @@ -331,4 +344,4 @@ class CustomQuizScreenState extends State ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/custom_srs_screen.dart b/lib/src/screens/custom_srs_screen.dart index 8a4861c..ad3c432 100644 --- a/lib/src/screens/custom_srs_screen.dart +++ b/lib/src/screens/custom_srs_screen.dart @@ -10,7 +10,8 @@ class CustomSrsScreen extends StatefulWidget { State createState() => _CustomSrsScreenState(); } -class _CustomSrsScreenState extends State with SingleTickerProviderStateMixin { +class _CustomSrsScreenState extends State + with SingleTickerProviderStateMixin { late TabController _tabController; final _deckRepository = CustomDeckRepository(); List _deck = []; @@ -49,7 +50,9 @@ class _CustomSrsScreenState extends State with SingleTickerProv } Future _updateCard(CustomKanjiItem item) async { - final index = _deck.indexWhere((element) => element.characters == item.characters); + final index = _deck.indexWhere( + (element) => element.characters == item.characters, + ); if (index != -1) { setState(() { _deck[index] = item; @@ -79,7 +82,8 @@ class _CustomSrsScreenState extends State with SingleTickerProv item.srsData.listeningComprehensionNextReview!.isBefore(now); }).toList(); - final allDecksEmpty = jpnToEngReviewDeck.isEmpty && + final allDecksEmpty = + jpnToEngReviewDeck.isEmpty && engToJpnReviewDeck.isEmpty && listeningReviewDeck.isEmpty; @@ -114,8 +118,8 @@ class _CustomSrsScreenState extends State with SingleTickerProv body: _deck.isEmpty ? const Center(child: Text('Add cards to start quizzing!')) : allDecksEmpty - ? const Center(child: Text('No cards due for review.')) - : TabBarView( + ? const Center(child: Text('No cards due for review.')) + : TabBarView( controller: _tabController, children: [ CustomQuizScreen( diff --git a/lib/src/screens/home_screen.dart b/lib/src/screens/home_screen.dart index a5d847e..06db77d 100644 --- a/lib/src/screens/home_screen.dart +++ b/lib/src/screens/home_screen.dart @@ -39,7 +39,8 @@ class HomeScreen extends StatefulWidget { State createState() => _HomeScreenState(); } -class _HomeScreenState extends State with SingleTickerProviderStateMixin { +class _HomeScreenState extends State + with SingleTickerProviderStateMixin { late TabController _tabController; List _deck = []; bool _loading = false; @@ -171,8 +172,10 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _deck.sort((a, b) { int getSrsStage(KanjiItem item) { if (mode == QuizMode.reading) { - final onyomiStage = item.srsItems['${QuizMode.reading}onyomi']?.srsStage; - final kunyomiStage = item.srsItems['${QuizMode.reading}kunyomi']?.srsStage; + final onyomiStage = + item.srsItems['${QuizMode.reading}onyomi']?.srsStage; + final kunyomiStage = + item.srsItems['${QuizMode.reading}kunyomi']?.srsStage; if (onyomiStage != null && kunyomiStage != null) { return min(onyomiStage, kunyomiStage); @@ -184,8 +187,10 @@ class _HomeScreenState extends State with SingleTickerProviderStateM DateTime getLastAsked(KanjiItem item) { if (mode == QuizMode.reading) { - final onyomiLastAsked = item.srsItems['${QuizMode.reading}onyomi']?.lastAsked; - final kunyomiLastAsked = item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked; + final onyomiLastAsked = + item.srsItems['${QuizMode.reading}onyomi']?.lastAsked; + final kunyomiLastAsked = + item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked; if (onyomiLastAsked != null && kunyomiLastAsked != null) { return onyomiLastAsked.isBefore(kunyomiLastAsked) @@ -227,16 +232,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM quizState.correctAnswers = [quizState.current!.meanings.first]; quizState.options = [ quizState.correctAnswers.first, - ..._dg.generateMeanings(quizState.current!, _deck, 3) - ].map(_toTitleCase).toList() - ..shuffle(); + ..._dg.generateMeanings(quizState.current!, _deck, 3), + ].map(_toTitleCase).toList()..shuffle(); break; case QuizMode.englishToKanji: quizState.correctAnswers = [quizState.current!.characters]; quizState.options = [ quizState.correctAnswers.first, - ..._dg.generateKanji(quizState.current!, _deck, 3) + ..._dg.generateKanji(quizState.current!, _deck, 3), ]..shuffle(); break; @@ -249,16 +253,18 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ? _deck.expand((k) => k.onyomi) : _deck.expand((k) => k.kunyomi); - final distractors = readingsSource - .where((r) => !quizState.correctAnswers.contains(r)) - .toSet() - .toList() + final distractors = + readingsSource + .where((r) => !quizState.correctAnswers.contains(r)) + .toSet() + .toList() ..shuffle(); quizState.options = ([ - quizState.correctAnswers[_random.nextInt(quizState.correctAnswers.length)], - ...distractors.take(3) - ]) - ..shuffle(); + quizState.correctAnswers[_random.nextInt( + quizState.correctAnswers.length, + )], + ...distractors.take(3), + ])..shuffle(); break; } @@ -279,26 +285,29 @@ class _HomeScreenState extends State with SingleTickerProviderStateM String readingType = ''; if (mode == QuizMode.reading) { - readingType = quizState.readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi'; + readingType = quizState.readingHint.contains("on'yomi") + ? 'onyomi' + : 'kunyomi'; } final srsKey = mode.toString() + readingType; var srsItem = current.srsItems[srsKey]; final isNew = srsItem == null; - final srsItemForUpdate = srsItem ??= - SrsItem(kanjiId: current.id, quizMode: mode, readingType: readingType); - - quizState.asked += 1; - - quizState.selectedOption = option; - - quizState.showResult = true; - - setState(() {}); // Trigger UI rebuild to show selected/correct colors - - - - if (isCorrect) { + final srsItemForUpdate = srsItem ??= SrsItem( + kanjiId: current.id, + quizMode: mode, + readingType: readingType, + ); + + quizState.asked += 1; + + quizState.selectedOption = option; + + quizState.showResult = true; + + setState(() {}); + + if (isCorrect) { quizState.score += 1; srsItemForUpdate.srsStage += 1; if (_playCorrectSound) { @@ -310,6 +319,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM srsItemForUpdate.lastAsked = DateTime.now(); current.srsItems[srsKey] = srsItemForUpdate; + final scaffoldMessenger = ScaffoldMessenger.of(context); + final theme = Theme.of(context); + if (isNew) { await repo.insertSrsItem(srsItemForUpdate); } else { @@ -319,26 +331,28 @@ class _HomeScreenState extends State with SingleTickerProviderStateM final correctDisplay = (mode == QuizMode.kanjiToEnglish) ? _toTitleCase(quizState.correctAnswers.first) : (mode == QuizMode.reading - ? quizState.correctAnswers.join(', ') - : quizState.correctAnswers.first); + ? quizState.correctAnswers.join(', ') + : quizState.correctAnswers.first); final snack = SnackBar( content: Text( isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', style: TextStyle( - color: isCorrect ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, + color: isCorrect + ? theme.colorScheme.primary + : theme.colorScheme.error, fontWeight: FontWeight.bold, ), ), - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + backgroundColor: theme.colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 900), ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(snack); + scaffoldMessenger.showSnackBar(snack); } setState(() { - _isAnswering = true; // Disable input after showing result + _isAnswering = true; }); Future.delayed(const Duration(milliseconds: 900), () { @@ -357,7 +371,12 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('WaniKani API key is not set.', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), + Text( + 'WaniKani API key is not set.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () async { @@ -389,11 +408,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM backgroundColor: Theme.of(context).colorScheme.surface, body: TabBarView( controller: _tabController, - children: [ - _buildQuizPage(0), - _buildQuizPage(1), - _buildQuizPage(2), - ], + children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)], ), ); } @@ -430,11 +445,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM Expanded( child: Text( _status, - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), ), if (_loading) - CircularProgressIndicator(color: Theme.of(context).colorScheme.primary), + CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), ], ), const SizedBox(height: 18), @@ -472,7 +491,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM const SizedBox(height: 8), Text( 'Score: ${quizState.score} / ${quizState.asked}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), ], ), @@ -481,4 +502,4 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/settings_screen.dart b/lib/src/screens/settings_screen.dart index 2d01a11..25b85e7 100644 --- a/lib/src/screens/settings_screen.dart +++ b/lib/src/screens/settings_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:hirameki_srs/src/models/theme_model.dart'; +import 'package:hirameki_srs/src/themes.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/deck_repository.dart'; @@ -50,24 +52,26 @@ class _SettingsScreenState extends State { await repo.setApiKey(apiKey); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('API key saved!')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('API key saved!'))); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const HomeScreen()), - ); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen())); } } @override Widget build(BuildContext context) { + final themeModel = Provider.of(context); + return Scaffold( - backgroundColor: const Color(0xFF121212), + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( title: const Text('Settings'), - backgroundColor: const Color(0xFF1F1F1F), - foregroundColor: Colors.white, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + foregroundColor: Theme.of(context).colorScheme.onSurface, ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -76,15 +80,19 @@ class _SettingsScreenState extends State { TextField( controller: _apiKeyController, obscureText: true, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: 'WaniKani API Key', - labelStyle: const TextStyle(color: Colors.grey), + labelStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), filled: true, - fillColor: const Color(0xFF1E1E1E), + fillColor: Theme.of(context).colorScheme.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: Colors.grey), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ), @@ -92,16 +100,18 @@ class _SettingsScreenState extends State { ElevatedButton( onPressed: _saveApiKey, style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - foregroundColor: Colors.white, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, ), child: const Text('Save & Start Quiz'), ), const SizedBox(height: 24), SwitchListTile( - title: const Text( + title: Text( 'Play audio for vocabulary', - style: TextStyle(color: Colors.white), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), value: _playAudio, onChanged: (value) async { @@ -111,18 +121,22 @@ class _SettingsScreenState extends State { _playAudio = value; }); }, - activeThumbColor: Colors.blueAccent, - inactiveThumbColor: Colors.grey, - tileColor: const Color(0xFF1E1E1E), + activeThumbColor: Theme.of(context).colorScheme.primary, + inactiveThumbColor: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + tileColor: Theme.of(context).colorScheme.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), const SizedBox(height: 12), SwitchListTile( - title: const Text( + title: Text( 'Play sound on correct answer', - style: TextStyle(color: Colors.white), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), value: _playCorrectSound, onChanged: (value) async { @@ -132,9 +146,50 @@ class _SettingsScreenState extends State { _playCorrectSound = value; }); }, - activeThumbColor: Colors.blueAccent, - inactiveThumbColor: Colors.grey, - tileColor: const Color(0xFF1E1E1E), + activeThumbColor: Theme.of(context).colorScheme.primary, + inactiveThumbColor: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + tileColor: Theme.of(context).colorScheme.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 12), + ListTile( + title: Text( + 'Theme', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + trailing: DropdownButton( + value: themeModel.currentTheme, + dropdownColor: Theme.of(context).colorScheme.surfaceContainer, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + items: [ + DropdownMenuItem( + value: Themes.dark, + child: const Text('Dark'), + ), + DropdownMenuItem( + value: Themes.light, + child: const Text('Light'), + ), + DropdownMenuItem( + value: Themes.nier, + child: const Text('Nier'), + ), + ], + onChanged: (theme) { + if (theme != null) { + themeModel.setTheme(theme); + } + }, + ), + tileColor: Theme.of(context).colorScheme.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index cc7773e..a8fa15d 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -31,7 +31,8 @@ class VocabScreen extends StatefulWidget { State createState() => _VocabScreenState(); } -class _VocabScreenState extends State with SingleTickerProviderStateMixin { +class _VocabScreenState extends State + with SingleTickerProviderStateMixin { late TabController _tabController; List _deck = []; bool _loading = false; @@ -150,7 +151,9 @@ class _VocabScreenState extends State with SingleTickerProviderStat List currentDeckForMode = _deck; if (mode == VocabQuizMode.audioToEnglish) { - currentDeckForMode = _deck.where((item) => item.pronunciationAudios.isNotEmpty).toList(); + currentDeckForMode = _deck + .where((item) => item.pronunciationAudios.isNotEmpty) + .toList(); if (currentDeckForMode.isEmpty) { setState(() { _status = 'No vocabulary with audio found.'; @@ -160,13 +163,16 @@ class _VocabScreenState extends State with SingleTickerProviderStat } } - // If it's a new session or we've gone through all shuffled items, re-shuffle - if (quizState.shuffledDeck.isEmpty || quizState.currentIndex >= quizState.shuffledDeck.length) { - quizState.shuffledDeck = currentDeckForMode.toList(); // Start with a fresh copy - // Apply sorting based on SRS stages here, but only once per shuffle + if (quizState.shuffledDeck.isEmpty || + quizState.currentIndex >= quizState.shuffledDeck.length) { + quizState.shuffledDeck = currentDeckForMode.toList(); quizState.shuffledDeck.sort((a, b) { - final aSrsItem = a.srsItems[mode.toString()] ?? VocabSrsItem(vocabId: a.id, quizMode: mode); - final bSrsItem = b.srsItems[mode.toString()] ?? VocabSrsItem(vocabId: b.id, quizMode: mode); + final aSrsItem = + a.srsItems[mode.toString()] ?? + VocabSrsItem(vocabId: a.id, quizMode: mode); + final bSrsItem = + b.srsItems[mode.toString()] ?? + VocabSrsItem(vocabId: b.id, quizMode: mode); final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage); if (stageComparison != 0) { return stageComparison; @@ -176,8 +182,8 @@ class _VocabScreenState extends State with SingleTickerProviderStat quizState.currentIndex = 0; } - quizState.current = quizState.shuffledDeck[quizState.currentIndex]; // Pick from shuffled deck - quizState.currentIndex++; // Advance index + quizState.current = quizState.shuffledDeck[quizState.currentIndex]; + quizState.currentIndex++; quizState.key = UniqueKey(); if (mode == VocabQuizMode.audioToEnglish) { @@ -195,16 +201,15 @@ class _VocabScreenState extends State with SingleTickerProviderStat quizState.correctAnswers = [quizState.current!.meanings.first]; quizState.options = [ quizState.correctAnswers.first, - ..._dg.generateVocabMeanings(quizState.current!, _deck, 3) - ].map(_toTitleCase).toList() - ..shuffle(); + ..._dg.generateVocabMeanings(quizState.current!, _deck, 3), + ].map(_toTitleCase).toList()..shuffle(); break; case VocabQuizMode.englishToVocab: quizState.correctAnswers = [quizState.current!.characters]; quizState.options = [ quizState.correctAnswers.first, - ..._dg.generateVocab(quizState.current!, _deck, 3) + ..._dg.generateVocab(quizState.current!, _deck, 3), ]..shuffle(); break; } @@ -218,14 +223,16 @@ class _VocabScreenState extends State with SingleTickerProviderStat final current = _currentQuizState.current; if (current == null || current.pronunciationAudios.isEmpty) return; - final maleAudios = current.pronunciationAudios.where((a) => a.gender == 'male'); - final audioUrl = (maleAudios.isNotEmpty ? maleAudios.first.url : current.pronunciationAudios.first.url); + final maleAudios = current.pronunciationAudios.where( + (a) => a.gender == 'male', + ); + final audioUrl = (maleAudios.isNotEmpty + ? maleAudios.first.url + : current.pronunciationAudios.first.url); try { await _audioPlayer.play(UrlSource(audioUrl)); - } catch (e) { - // Ignore player errors - } + } finally {} } void _answer(String option) async { @@ -248,7 +255,7 @@ class _VocabScreenState extends State with SingleTickerProviderStat quizState.asked += 1; quizState.selectedOption = option; quizState.showResult = true; - setState(() {}); // Trigger UI rebuild to show selected/correct colors + setState(() {}); if (isCorrect) { quizState.score += 1; @@ -269,28 +276,28 @@ class _VocabScreenState extends State with SingleTickerProviderStat ? _toTitleCase(quizState.correctAnswers.first) : quizState.correctAnswers.first; + if (!mounted) return; final snack = SnackBar( content: Text( isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', style: TextStyle( - color: isCorrect ? Colors.greenAccent : Colors.redAccent, + color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold, ), ), - backgroundColor: const Color(0xFF222222), + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 900), ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(snack); - } + ScaffoldMessenger.of(context).showSnackBar(snack); if (isCorrect) { if (_playCorrectSound) { await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); } if (_playAudio && mode != VocabQuizMode.audioToEnglish) { - final maleAudios = - current.pronunciationAudios.where((a) => a.gender == 'male'); + final maleAudios = current.pronunciationAudios.where( + (a) => a.gender == 'male', + ); if (maleAudios.isNotEmpty) { final completer = Completer(); final sub = _audioPlayer.onPlayerComplete.listen((event) { @@ -300,19 +307,15 @@ class _VocabScreenState extends State with SingleTickerProviderStat try { await _audioPlayer.play(UrlSource(maleAudios.first.url)); await completer.future.timeout(const Duration(seconds: 5)); - } catch (e) { - // Ignore player errors } finally { await sub.cancel(); } } } - } else { - // No fixed delay for incorrect answers } setState(() { - _isAnswering = true; // Disable input after showing result + _isAnswering = true; }); _nextQuestion(); @@ -327,13 +330,14 @@ class _VocabScreenState extends State with SingleTickerProviderStat child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('WaniKani API key is not set.'), + Text('WaniKani API key is not set.', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), const SizedBox(height: 16), ElevatedButton( onPressed: () async { await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); + if (!mounted) return; _loadDeck(); }, child: const Text('Go to Settings'), @@ -358,11 +362,7 @@ class _VocabScreenState extends State with SingleTickerProviderStat ), body: TabBarView( controller: _tabController, - children: [ - _buildQuizPage(0), - _buildQuizPage(1), - _buildQuizPage(2), - ], + children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)], ), ); } @@ -377,7 +377,7 @@ class _VocabScreenState extends State with SingleTickerProviderStat promptWidget = const SizedBox.shrink(); } else if (mode == VocabQuizMode.audioToEnglish) { promptWidget = IconButton( - icon: const Icon(Icons.volume_up, color: Colors.white, size: 64), + icon: Icon(Icons.volume_up, color: Theme.of(context).colorScheme.onSurface, size: 64), onPressed: _playCurrentAudio, ); } else { @@ -390,10 +390,12 @@ class _VocabScreenState extends State with SingleTickerProviderStat promptText = _toTitleCase(quizState.current!.meanings.first); break; case VocabQuizMode.audioToEnglish: - // Handled above break; } - promptWidget = Text(promptText, style: const TextStyle(fontSize: 48, color: Colors.white)); + promptWidget = Text( + promptText, + style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface), + ); } return Padding( @@ -403,13 +405,9 @@ class _VocabScreenState extends State with SingleTickerProviderStat children: [ Row( children: [ - Expanded( - child: Text( - _status, - ), - ), + Expanded(child: Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface))), if (_loading) - const CircularProgressIndicator(color: Colors.blueAccent), + CircularProgressIndicator(color: Theme.of(context).colorScheme.primary), ], ), const SizedBox(height: 18), @@ -422,10 +420,7 @@ class _VocabScreenState extends State with SingleTickerProviderStat maxWidth: 500, minHeight: 150, ), - child: KanjiCard( - characterWidget: promptWidget, - subtitle: '', - ), + child: KanjiCard(characterWidget: promptWidget, subtitle: ''), ), ), ), @@ -445,7 +440,7 @@ class _VocabScreenState extends State with SingleTickerProviderStat const SizedBox(height: 8), Text( 'Score: ${quizState.score} / ${quizState.asked}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), ], ), @@ -454,4 +449,4 @@ class _VocabScreenState extends State with SingleTickerProviderStat ), ); } -} \ No newline at end of file +} diff --git a/lib/src/services/deck_repository.dart b/lib/src/services/deck_repository.dart index aeb6b99..859be03 100644 --- a/lib/src/services/deck_repository.dart +++ b/lib/src/services/deck_repository.dart @@ -103,8 +103,6 @@ class DeckRepository { try { envApiKey = dotenv.env['WANIKANI_API_KEY']; } catch (e) { - // dotenv is not initialized, so we can't get the key. - // This is expected in release builds. envApiKey = null; } @@ -261,6 +259,4 @@ class DeckRepository { await saveKanji(items); return items; } - - -} \ No newline at end of file +} diff --git a/lib/src/services/vocab_deck_repository.dart b/lib/src/services/vocab_deck_repository.dart index cc25efa..ecbb2dd 100644 --- a/lib/src/services/vocab_deck_repository.dart +++ b/lib/src/services/vocab_deck_repository.dart @@ -104,8 +104,6 @@ class VocabDeckRepository { try { envApiKey = dotenv.env['WANIKANI_API_KEY']; } catch (e) { - // dotenv is not initialized, so we can't get the key. - // This is expected in release builds. envApiKey = null; } @@ -205,9 +203,7 @@ class VocabDeckRepository { ), ); } - } catch (e) { - // Error decoding, so we'll just have no audio for this item - } + } finally {} } return VocabularyItem( id: r['id'] as int, diff --git a/lib/src/themes.dart b/lib/src/themes.dart new file mode 100644 index 0000000..7092f68 --- /dev/null +++ b/lib/src/themes.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +class SrsColors { + final Color level1; + final Color level2; + final Color level3; + final Color level4; + final Color level5; + final Color level6; + final Color level7; + final Color level8; + final Color level9; + + const SrsColors({ + required this.level1, + required this.level2, + required this.level3, + required this.level4, + required this.level5, + required this.level6, + required this.level7, + required this.level8, + required this.level9, + }); +} + +extension CustomTheme on ThemeData { + SrsColors get srsColors { + if (brightness == Brightness.dark) { + return const SrsColors( + level1: Color(0xFFE57373), // red + level2: Color(0xFFFFB74D), // orange + level3: Color(0xFFFFD54F), // yellow + level4: Color(0xFFDCE775), // lime + level5: Color(0xFFAED581), // light green + level6: Color(0xFF81C784), // green + level7: Color(0xFF4DB6AC), // teal + level8: Color(0xFF4FC3F7), // light blue + level9: Color(0xFF7986CB), // indigo + ); + } else if (colorScheme.primary == const Color(0xFF7B6D53)) { // Nier theme + return const SrsColors( + level1: Color(0xFFB71C1C), // dark red + level2: Color(0xFFD84315), // deep orange + level3: Color(0xFFF57F17), // yellow + level4: Color(0xFF9E9D24), // lime + level5: Color(0xFF558B2F), // light green + level6: Color(0xFF2E7D32), // green + level7: Color(0xFF00695C), // teal + level8: Color(0xFF0277BD), // light blue + level9: Color(0xFF283593), // indigo + ); + } else { // Light theme + return const SrsColors( + level1: Colors.red, + level2: Colors.orange, + level3: Colors.yellow, + level4: Colors.lightGreen, + level5: Colors.green, + level6: Colors.teal, + level7: Colors.cyan, + level8: Colors.blue, + level9: Colors.purple, + ); + } + } +} + +class Themes { + static final dark = ThemeData( + colorScheme: const ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFF90CAF9), + onPrimary: Colors.black, + secondary: Color(0xFFBBDEFB), + onSecondary: Colors.black, + tertiary: Color(0xFFA5D6A7), + onTertiary: Colors.black, + error: Color(0xFFEF9A9A), + onError: Colors.black, + surface: Color(0xFF121212), + onSurface: Colors.white, + surfaceContainer: Color(0xFF1E1E1E), + surfaceContainerHighest: Color(0xFF424242), + onSurfaceVariant: Colors.white70, + ), + useMaterial3: true, + ); + + static final light = ThemeData( + colorScheme: const ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF1976D2), + onPrimary: Colors.white, + secondary: Color(0xFF42A5F5), + onSecondary: Colors.white, + tertiary: Color(0xFF66BB6A), + onTertiary: Colors.white, + error: Color(0xFFE57373), + onError: Colors.white, + surface: Color(0xFFFFFFFF), + onSurface: Colors.black, + surfaceContainer: Color(0xFFF5F5F5), + surfaceContainerHighest: Color(0xFFE0E0E0), + onSurfaceVariant: Colors.black54, + ), + useMaterial3: true, + ); + + static final nier = ThemeData( + colorScheme: const ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF7B6D53), + onPrimary: Colors.white, + secondary: Color(0xFFA99A7E), + onSecondary: Colors.white, + tertiary: Color(0xFFA99A7E), + onTertiary: Colors.white, + error: Color(0xFFD32F2F), + onError: Colors.white, + surface: Color(0xFFCFCBAA), + onSurface: Color(0xFF333333), + surfaceContainer: Color(0xFFBDB898), + surfaceContainerHighest: Color(0xFFA8A388), + onSurfaceVariant: Color(0xFF545454), + ), + useMaterial3: true, + ); +}