diff --git a/lib/src/api/wk_client.dart b/lib/src/api/wk_client.dart index 2b21854..2991e9a 100644 --- a/lib/src/api/wk_client.dart +++ b/lib/src/api/wk_client.dart @@ -88,6 +88,7 @@ class WkClient { } return out; } + static Subject createSubjectFromMap(Map map) { final String object = map['object']; if (object == 'kanji') { diff --git a/lib/src/models/subject.dart b/lib/src/models/subject.dart index 36aac3b..35aa09d 100644 --- a/lib/src/models/subject.dart +++ b/lib/src/models/subject.dart @@ -35,4 +35,4 @@ abstract class Subject { 'data': data, }; } -} \ No newline at end of file +} diff --git a/lib/src/screens/browse_screen.dart b/lib/src/screens/browse_screen.dart index b5875ab..0c516f7 100644 --- a/lib/src/screens/browse_screen.dart +++ b/lib/src/screens/browse_screen.dart @@ -254,7 +254,7 @@ class _BrowseScreenState extends State backgroundColor: isSelected ? Theme.of(context).colorScheme.primary : isDisabled - ? Theme.of(context).colorScheme.surfaceVariant + ? Theme.of(context).colorScheme.surfaceContainerHighest : Theme.of(context).colorScheme.surfaceContainerHighest, foregroundColor: isSelected diff --git a/lib/src/screens/custom_quiz_screen.dart b/lib/src/screens/custom_quiz_screen.dart index bae38a7..74f52d9 100644 --- a/lib/src/screens/custom_quiz_screen.dart +++ b/lib/src/screens/custom_quiz_screen.dart @@ -34,34 +34,37 @@ class CustomQuizScreen extends StatefulWidget { State createState() => CustomQuizScreenState(); } +class _CustomQuizState { + CustomKanjiItem? current; + List options = []; + List correctAnswers = []; + int score = 0; + int asked = 0; + Key key = UniqueKey(); + String? selectedOption; + bool showResult = false; + Set wrongItems = {}; +} + class CustomQuizScreenState extends State with TickerProviderStateMixin { - int _currentIndex = 0; + final _quizState = _CustomQuizState(); List _shuffledDeck = []; - List _options = []; - bool _answered = false; - bool? _correct; + int _sessionDeckSize = 0; + bool _isAnswering = false; late AnimationController _shakeController; late Animation _shakeAnimation; - final List _incorrectlyAnsweredItems = []; final _audioPlayer = AudioPlayer(); bool _playIncorrectSound = true; bool _playCorrectSound = true; bool _playNarrator = true; - String? _selectedOption; - String? _correctAnswer; - bool _showResult = false; - @override void initState() { super.initState(); _shuffledDeck = widget.deck.toList()..shuffle(); - if (_shuffledDeck.isNotEmpty) { - _generateOptions(); - } - + _sessionDeckSize = _shuffledDeck.length; _shakeController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, @@ -70,6 +73,7 @@ class CustomQuizScreenState extends State CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn), ); _loadSettings(); + _nextQuestion(); } Future _loadSettings() async { @@ -90,163 +94,53 @@ class CustomQuizScreenState extends State void didUpdateWidget(CustomQuizScreen oldWidget) { super.didUpdateWidget(oldWidget); if (widget.deck != oldWidget.deck && !widget.isActive) { - setState(() { - _shuffledDeck = widget.deck.toList()..shuffle(); - _currentIndex = 0; - _answered = false; - _correct = null; - if (_shuffledDeck.isNotEmpty) { - _generateOptions(); - } - }); + _shuffledDeck = widget.deck.toList()..shuffle(); + _sessionDeckSize = _shuffledDeck.length; + _nextQuestion(); } if (widget.useKanji != oldWidget.useKanji) { - setState(() { - _generateOptions(); - }); + _nextQuestion(); } } void playAudio() async { + final quizState = _quizState; if (widget.quizMode == CustomQuizMode.listeningComprehension && - _currentIndex < _shuffledDeck.length && _playNarrator) { + quizState.current != null && + _playNarrator) { final ttsService = Provider.of(context, listen: false); - await ttsService.speak(_shuffledDeck[_currentIndex].characters); + await ttsService.speak(quizState.current!.characters); } } - - @override void dispose() { _shakeController.dispose(); super.dispose(); } - void _generateOptions() { - final currentItem = _shuffledDeck[_currentIndex]; - if (widget.quizMode == CustomQuizMode.listeningComprehension || - widget.quizMode == CustomQuizMode.japaneseToEnglish) { - _options = [currentItem.meaning]; - } - else { - _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) { - _options.add(otherItems[i].meaning); - } else { - _options.add( - widget.useKanji && otherItems[i].kanji != null - ? otherItems[i].kanji! - : otherItems[i].characters, - ); - } - } - while (_options.length < 4) { - _options.add('---'); - } - _options.shuffle(); - } - - void _checkAnswer(String answer) async { - final currentItem = _shuffledDeck[_currentIndex]; - final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese) - ? (widget.useKanji && currentItem.kanji != null - ? currentItem.kanji! - : currentItem.characters) - : currentItem.meaning; - final isCorrect = answer == correctAnswer; + void _answer(String option) async { + final quizState = _quizState; + final current = quizState.current!; + final isCorrect = quizState.correctAnswers + .map((a) => a.toLowerCase().trim()) + .contains(option.toLowerCase().trim()); setState(() { - _selectedOption = answer; - _correctAnswer = correctAnswer; - _showResult = true; + quizState.selectedOption = option; + quizState.showResult = true; + _isAnswering = true; }); - int currentSrsLevel = 0; // Initialize with a default value - if (currentItem.useInterval) { - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentSrsLevel = currentItem.srsData.japaneseToEnglish; - break; - case CustomQuizMode.englishToJapanese: - currentSrsLevel = currentItem.srsData.englishToJapanese; - break; - case CustomQuizMode.listeningComprehension: - currentSrsLevel = currentItem.srsData.listeningComprehension; - break; - } - - if (isCorrect) { - if (_incorrectlyAnsweredItems.contains(currentItem.characters)) { - _incorrectlyAnsweredItems.remove(currentItem.characters); - } else { - currentSrsLevel++; - } - final interval = pow(2, currentSrsLevel).toInt(); - final newNextReview = DateTime.now().add(Duration(hours: interval)); - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentItem.srsData.japaneseToEnglishNextReview = newNextReview; - break; - case CustomQuizMode.englishToJapanese: - currentItem.srsData.englishToJapaneseNextReview = newNextReview; - break; - case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = - newNextReview; - break; - } - } else { - if (!_incorrectlyAnsweredItems.contains(currentItem.characters)) { - _incorrectlyAnsweredItems.add(currentItem.characters); - } - currentSrsLevel = max(0, currentSrsLevel - 1); - final newNextReview = DateTime.now().add(const Duration(hours: 1)); - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentItem.srsData.japaneseToEnglishNextReview = newNextReview; - break; - case CustomQuizMode.englishToJapanese: - currentItem.srsData.englishToJapaneseNextReview = newNextReview; - break; - case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = - newNextReview; - break; - } - } - - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentItem.srsData.japaneseToEnglish = currentSrsLevel; - break; - case CustomQuizMode.englishToJapanese: - currentItem.srsData.englishToJapanese = currentSrsLevel; - break; - case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehension = currentSrsLevel; - break; - } - - widget.onCardReviewed(currentItem); + if (current.useInterval) { + _updateSrsLevel(current, isCorrect); } final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese) - ? (widget.useKanji && currentItem.kanji != null - ? currentItem.kanji! - : currentItem.characters) - : currentItem.meaning; + ? (widget.useKanji && current.kanji != null + ? current.kanji! + : current.characters) + : current.meaning; final snack = SnackBar( content: Text( @@ -266,52 +160,23 @@ class CustomQuizScreenState extends State } if (isCorrect) { - if (_incorrectlyAnsweredItems.contains(currentItem.characters)) { - _incorrectlyAnsweredItems.remove(currentItem.characters); - } else { - currentSrsLevel++; - } - final interval = pow(2, currentSrsLevel).toInt(); - final newNextReview = DateTime.now().add(Duration(hours: interval)); - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentItem.srsData.japaneseToEnglishNextReview = newNextReview; - break; - case CustomQuizMode.englishToJapanese: - currentItem.srsData.englishToJapaneseNextReview = newNextReview; - break; - case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = - newNextReview; - break; + quizState.asked += 1; + if (!quizState.wrongItems.contains(current.characters)) { + quizState.score += 1; } if (_playCorrectSound && !_playNarrator) { await _audioPlayer.play(AssetSource('sfx/correct.wav')); } else if (_playNarrator) { if (widget.quizMode == CustomQuizMode.japaneseToEnglish || widget.quizMode == CustomQuizMode.englishToJapanese) { - await _speak(currentItem.characters); + await _speak(current.characters); } } await Future.delayed(const Duration(milliseconds: 500)); } else { - if (!_incorrectlyAnsweredItems.contains(currentItem.characters)) { - _incorrectlyAnsweredItems.add(currentItem.characters); - } - currentSrsLevel = max(0, currentSrsLevel - 1); - final newNextReview = DateTime.now().add(const Duration(hours: 1)); - switch (widget.quizMode) { - case CustomQuizMode.japaneseToEnglish: - currentItem.srsData.japaneseToEnglishNextReview = newNextReview; - break; - case CustomQuizMode.englishToJapanese: - currentItem.srsData.englishToJapaneseNextReview = newNextReview; - break; - case CustomQuizMode.listeningComprehension: - currentItem.srsData.listeningComprehensionNextReview = - newNextReview; - break; - } + quizState.wrongItems.add(current.characters); + _shuffledDeck.add(current); + _shuffledDeck.shuffle(); if (_playIncorrectSound) { await _audioPlayer.play(AssetSource('sfx/incorrect.wav')); } @@ -319,24 +184,132 @@ class CustomQuizScreenState extends State await Future.delayed(const Duration(milliseconds: 900)); } - _nextQuestion(); - } - - Future _nextQuestion() async { - setState(() { - _currentIndex++; - _answered = false; - _correct = null; - _selectedOption = null; - _correctAnswer = null; - _showResult = false; - if (_currentIndex < _shuffledDeck.length) { - _generateOptions(); + Future.delayed(const Duration(milliseconds: 900), () { + if (mounted) { + _nextQuestion(); } }); - if (_currentIndex < _shuffledDeck.length && + } + + void _updateSrsLevel(CustomKanjiItem item, bool isCorrect) { + int currentSrsLevel = 0; + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + currentSrsLevel = item.srsData.japaneseToEnglish; + break; + case CustomQuizMode.englishToJapanese: + currentSrsLevel = item.srsData.englishToJapanese; + break; + case CustomQuizMode.listeningComprehension: + currentSrsLevel = item.srsData.listeningComprehension; + break; + } + + if (isCorrect) { + currentSrsLevel++; + final interval = pow(2, currentSrsLevel).toInt(); + final newNextReview = DateTime.now().add(Duration(hours: interval)); + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + item.srsData.japaneseToEnglishNextReview = newNextReview; + break; + case CustomQuizMode.englishToJapanese: + item.srsData.englishToJapaneseNextReview = newNextReview; + break; + case CustomQuizMode.listeningComprehension: + item.srsData.listeningComprehensionNextReview = newNextReview; + break; + } + } else { + currentSrsLevel = max(0, currentSrsLevel - 1); + final newNextReview = DateTime.now().add(const Duration(hours: 1)); + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + item.srsData.japaneseToEnglishNextReview = newNextReview; + break; + case CustomQuizMode.englishToJapanese: + item.srsData.englishToJapaneseNextReview = newNextReview; + break; + case CustomQuizMode.listeningComprehension: + item.srsData.listeningComprehensionNextReview = newNextReview; + break; + } + } + + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + item.srsData.japaneseToEnglish = currentSrsLevel; + break; + case CustomQuizMode.englishToJapanese: + item.srsData.englishToJapanese = currentSrsLevel; + break; + case CustomQuizMode.listeningComprehension: + item.srsData.listeningComprehension = currentSrsLevel; + break; + } + + widget.onCardReviewed(item); + } + + void _nextQuestion() { + final quizState = _quizState; + + if (_shuffledDeck.isEmpty) { + setState(() { + quizState.current = null; + }); + return; + } + + quizState.current = _shuffledDeck.removeAt(0); + quizState.key = UniqueKey(); + + quizState.correctAnswers = []; + quizState.options = []; + quizState.selectedOption = null; + quizState.showResult = false; + + if (widget.quizMode == CustomQuizMode.japaneseToEnglish || widget.quizMode == CustomQuizMode.listeningComprehension) { - await _speak(_shuffledDeck[_currentIndex].characters); + quizState.correctAnswers = [quizState.current!.meaning]; + quizState.options = [quizState.correctAnswers.first]; + } else { + quizState.correctAnswers = [ + widget.useKanji && quizState.current!.kanji != null + ? quizState.current!.kanji! + : quizState.current!.characters, + ]; + quizState.options = [quizState.correctAnswers.first]; + } + + final otherItems = widget.deck + .where((item) => item.characters != quizState.current!.characters) + .toList(); + otherItems.shuffle(); + + for (var i = 0; i < min(3, otherItems.length); i++) { + if (widget.quizMode == CustomQuizMode.japaneseToEnglish || + widget.quizMode == CustomQuizMode.listeningComprehension) { + quizState.options.add(otherItems[i].meaning); + } else { + quizState.options.add( + widget.useKanji && otherItems[i].kanji != null + ? otherItems[i].kanji! + : otherItems[i].characters, + ); + } + } + while (quizState.options.length < 4) { + quizState.options.add('---'); + } + quizState.options.shuffle(); + + setState(() { + _isAnswering = false; + }); + + if (widget.quizMode == CustomQuizMode.listeningComprehension) { + _speak(quizState.current!.characters); } } @@ -346,25 +319,29 @@ class CustomQuizScreenState extends State } void _onOptionSelected(String option) { - if (!(_answered && _correct!)) { - _checkAnswer(option); + if (!_isAnswering) { + _answer(option); } } @override Widget build(BuildContext context) { - if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) { - return Center(child: Text('Review session complete!', style: TextStyle(color: Theme.of(context).colorScheme.onSurface))); + final quizState = _quizState; + + if (quizState.current == null) { + 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); + final currentItem = quizState.current!; Widget promptWidget; + String subtitle = ''; + if (widget.quizMode == CustomQuizMode.listeningComprehension) { promptWidget = IconButton( icon: const Icon(Icons.volume_up, size: 64), @@ -372,25 +349,60 @@ class CustomQuizScreenState extends State ); } else if (widget.quizMode == CustomQuizMode.englishToJapanese) { promptWidget = Text( - question, - style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface), + currentItem.meaning, + style: TextStyle( + fontSize: 48, + color: Theme.of(context).colorScheme.onSurface, + ), textAlign: TextAlign.center, ); } else { + final promptText = widget.useKanji && currentItem.kanji != null + ? currentItem.kanji! + : currentItem.characters; promptWidget = GestureDetector( - onTap: () => _speak(question), + onTap: () => _speak(promptText), child: Text( - question, - style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface), + promptText, + style: TextStyle( + fontSize: 48, + color: Theme.of(context).colorScheme.onSurface, + ), textAlign: TextAlign.center, ), ); } return Padding( + key: quizState.key, padding: const EdgeInsets.all(16.0), child: Column( children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${quizState.asked} / $_sessionDeckSize', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: _sessionDeckSize > 0 + ? quizState.asked / _sessionDeckSize + : 0, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), const SizedBox(height: 18), Expanded( flex: 3, @@ -401,7 +413,10 @@ class CustomQuizScreenState extends State maxWidth: 500, minHeight: 150, ), - child: KanjiCard(characterWidget: promptWidget, subtitle: ''), + child: KanjiCard( + characterWidget: promptWidget, + subtitle: subtitle, + ), ), ), ), @@ -419,11 +434,18 @@ class CustomQuizScreenState extends State ); }, child: OptionsGrid( - options: _options, - onSelected: _onOptionSelected, - correctAnswers: [], - showResult: false, - isDisabled: false, + options: quizState.options, + onSelected: _isAnswering ? (option) {} : _onOptionSelected, + selectedOption: quizState.selectedOption, + correctAnswers: quizState.correctAnswers, + showResult: quizState.showResult, + ), + ), + const SizedBox(height: 8), + Text( + 'Score: ${quizState.score} / ${quizState.asked}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, ), ), ], diff --git a/lib/src/screens/home_screen.dart b/lib/src/screens/home_screen.dart index aa0b472..93b2d0f 100644 --- a/lib/src/screens/home_screen.dart +++ b/lib/src/screens/home_screen.dart @@ -128,8 +128,11 @@ class _HomeScreenState extends State } itemsByLevel.forEach((level, items) { - final allSrsItems = items.expand((item) => item.srsItems.values).toList(); - if (allSrsItems.isNotEmpty && allSrsItems.every((srs) => srs.disabled)) { + final allSrsItems = items + .expand((item) => item.srsItems.values) + .toList(); + if (allSrsItems.isNotEmpty && + allSrsItems.every((srs) => srs.disabled)) { disabledLevels.add(level); } }); @@ -144,9 +147,11 @@ class _HomeScreenState extends State if (mode == QuizMode.reading) { final onyomiSrs = item.srsItems['${QuizMode.reading}onyomi']; final kunyomiSrs = item.srsItems['${QuizMode.reading}kunyomi']; - final hasOnyomi = item.onyomi.isNotEmpty && + final hasOnyomi = + item.onyomi.isNotEmpty && (onyomiSrs == null || !onyomiSrs.disabled); - final hasKunyomi = item.kunyomi.isNotEmpty && + final hasKunyomi = + item.kunyomi.isNotEmpty && (kunyomiSrs == null || !kunyomiSrs.disabled); return hasOnyomi || hasKunyomi; } @@ -289,8 +294,9 @@ class _HomeScreenState extends State 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; @@ -341,8 +347,8 @@ class _HomeScreenState extends State 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( @@ -449,7 +455,9 @@ class _HomeScreenState extends State child: Text( _status, style: TextStyle( - fontSize: 24, color: Theme.of(context).colorScheme.onSurface), + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + ), ), ); } @@ -495,10 +503,12 @@ class _HomeScreenState extends State value: (_sessionDeckSizes[index] ?? 0) > 0 ? quizState.asked / (_sessionDeckSizes[index] ?? 1) : 0, - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary), + Theme.of(context).colorScheme.primary, + ), ), ], ), diff --git a/lib/src/screens/start_screen.dart b/lib/src/screens/start_screen.dart index adb2ad9..c4984ca 100644 --- a/lib/src/screens/start_screen.dart +++ b/lib/src/screens/start_screen.dart @@ -17,9 +17,9 @@ class StartScreen extends StatelessWidget { IconButton( icon: const Icon(Icons.settings), onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SettingsScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const SettingsScreen())); }, ), ], @@ -48,9 +48,9 @@ class StartScreen extends StatelessWidget { icon: Icons.extension, description: 'Test your knowledge of kanji characters.', onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const HomeScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const HomeScreen())); }, ), _buildModeCard( @@ -59,9 +59,9 @@ class StartScreen extends StatelessWidget { icon: Icons.school, description: 'Practice vocabulary from your WaniKani deck.', onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const VocabScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const VocabScreen())); }, ), _buildModeCard( @@ -70,9 +70,9 @@ class StartScreen extends StatelessWidget { icon: Icons.grid_view, description: 'Look through your kanji and vocabulary decks.', onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const BrowseScreen()), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const BrowseScreen())); }, ), _buildModeCard( @@ -92,7 +92,8 @@ class StartScreen extends StatelessWidget { ); } - Widget _buildModeCard(BuildContext context, { + Widget _buildModeCard( + BuildContext context, { required String title, required IconData icon, required String description, @@ -109,13 +110,17 @@ class StartScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 48, color: Theme.of(context).colorScheme.primary), + Icon( + icon, + size: 48, + color: Theme.of(context).colorScheme.primary, + ), const SizedBox(height: 16), Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 8), @@ -135,4 +140,4 @@ class StartScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index abbbc59..387cf52 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -123,8 +123,11 @@ class _VocabScreenState extends State } itemsByLevel.forEach((level, items) { - final allSrsItems = items.expand((item) => item.srsItems.values).toList(); - if (allSrsItems.isNotEmpty && allSrsItems.every((srs) => srs.disabled)) { + final allSrsItems = items + .expand((item) => item.srsItems.values) + .toList(); + if (allSrsItems.isNotEmpty && + allSrsItems.every((srs) => srs.disabled)) { disabledLevels.add(level); } }); @@ -349,7 +352,7 @@ class _VocabScreenState extends State if (mounted) { _nextQuestion(); } - });; + }); } @override @@ -429,7 +432,9 @@ class _VocabScreenState extends State child: Text( _status, style: TextStyle( - fontSize: 24, color: Theme.of(context).colorScheme.onSurface), + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + ), ), ); } @@ -491,10 +496,12 @@ class _VocabScreenState extends State value: (_sessionDeckSizes[index] ?? 0) > 0 ? quizState.asked / (_sessionDeckSizes[index] ?? 1) : 0, - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary), + Theme.of(context).colorScheme.primary, + ), ), ], ), diff --git a/lib/src/services/custom_deck_repository.dart b/lib/src/services/custom_deck_repository.dart index b61790f..b2258d0 100644 --- a/lib/src/services/custom_deck_repository.dart +++ b/lib/src/services/custom_deck_repository.dart @@ -1,4 +1,3 @@ - import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/custom_kanji_item.dart'; @@ -29,7 +28,9 @@ class CustomDeckRepository { Future updateCards(List itemsToUpdate) async { final deck = await getCustomDeck(); for (var item in itemsToUpdate) { - final index = deck.indexWhere((element) => element.characters == item.characters); + final index = deck.indexWhere( + (element) => element.characters == item.characters, + ); if (index != -1) { deck[index] = item; } diff --git a/lib/src/services/database_constants.dart b/lib/src/services/database_constants.dart index 68baa40..a85a46a 100644 --- a/lib/src/services/database_constants.dart +++ b/lib/src/services/database_constants.dart @@ -1,4 +1,3 @@ - class DbConstants { static const String settingsTable = 'settings'; static const String kanjiTable = 'kanji'; diff --git a/lib/src/services/database_helper.dart b/lib/src/services/database_helper.dart index 96d5eec..de39333 100644 --- a/lib/src/services/database_helper.dart +++ b/lib/src/services/database_helper.dart @@ -52,8 +52,12 @@ class DatabaseHelper { }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 8) { - await db.execute('ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0'); - await db.execute('ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0'); + await db.execute( + 'ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0', + ); + await db.execute( + 'ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0', + ); } }, ); diff --git a/lib/src/services/deck_repository.dart b/lib/src/services/deck_repository.dart index c75c073..dc203ca 100644 --- a/lib/src/services/deck_repository.dart +++ b/lib/src/services/deck_repository.dart @@ -123,7 +123,8 @@ class DeckRepository { final db = await DatabaseHelper().db; final batch = db.batch(); for (final item in items) { - var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?'; + var where = + '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?'; final whereArgs = [item.subjectId, item.quizMode.toString()]; if (item.readingType != null) { where += ' AND ${DbConstants.readingTypeColumn} = ?'; @@ -148,7 +149,8 @@ class DeckRepository { Future updateSrsItem(SrsItem item) async { final db = await DatabaseHelper().db; - var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?'; + var where = + '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?'; final whereArgs = [item.subjectId, item.quizMode.toString()]; if (item.readingType != null) { where += ' AND ${DbConstants.readingTypeColumn} = ?'; diff --git a/lib/src/services/distractor_generator.dart b/lib/src/services/distractor_generator.dart index 984c9ed..4c557ac 100644 --- a/lib/src/services/distractor_generator.dart +++ b/lib/src/services/distractor_generator.dart @@ -5,14 +5,26 @@ import 'dart:math'; class DistractorGenerator { final Random _rnd = Random(); - List generateMeanings(KanjiItem correct, List pool, int needed) { + List generateMeanings( + KanjiItem correct, + List pool, + int needed, + ) { final correctMeaning = correct.meanings.first; - final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet(); + final tokens = correctMeaning + .split(RegExp(r'\s+')) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toSet(); final candidates = []; for (final k in pool) { if (k.id == correct.id) continue; for (final m in k.meanings) { - final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet(); + final mTokens = m + .split(RegExp(r'\s+')) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toSet(); if (mTokens.intersection(tokens).isNotEmpty) { candidates.add(m); } @@ -39,8 +51,15 @@ class DistractorGenerator { return out; } - List generateKanji(KanjiItem correct, List pool, int needed) { - final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList(); + List generateKanji( + KanjiItem correct, + List pool, + int needed, + ) { + final others = pool + .map((k) => k.characters) + .where((c) => c != correct.characters) + .toList(); others.shuffle(_rnd); final out = []; for (final o in others) { @@ -53,7 +72,11 @@ class DistractorGenerator { return out; } - List generateReadings(String correct, List pool, int needed) { + List generateReadings( + String correct, + List pool, + int needed, + ) { final poolReadings = []; for (final k in pool) { poolReadings.addAll(k.onyomi); @@ -72,14 +95,26 @@ class DistractorGenerator { return out; } - List generateVocabMeanings(VocabularyItem correct, List pool, int needed) { + List generateVocabMeanings( + VocabularyItem correct, + List pool, + int needed, + ) { final correctMeaning = correct.meanings.first; - final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet(); + final tokens = correctMeaning + .split(RegExp(r'\s+')) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toSet(); final candidates = []; for (final k in pool) { if (k.id == correct.id) continue; for (final m in k.meanings) { - final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet(); + final mTokens = m + .split(RegExp(r'\s+')) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toSet(); if (mTokens.intersection(tokens).isNotEmpty) { candidates.add(m); } @@ -106,8 +141,15 @@ class DistractorGenerator { return out; } - List generateVocab(VocabularyItem correct, List pool, int needed) { - final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList(); + List generateVocab( + VocabularyItem correct, + List pool, + int needed, + ) { + final others = pool + .map((k) => k.characters) + .where((c) => c != correct.characters) + .toList(); others.shuffle(_rnd); final out = []; for (final o in others) { @@ -121,4 +163,7 @@ class DistractorGenerator { } } -String _toTitleCase(String s) => s.split(' ').map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1))).join(' '); +String _toTitleCase(String s) => s + .split(' ') + .map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1))) + .join(' '); diff --git a/lib/src/services/vocab_deck_repository.dart b/lib/src/services/vocab_deck_repository.dart index 22fae25..46c000f 100644 --- a/lib/src/services/vocab_deck_repository.dart +++ b/lib/src/services/vocab_deck_repository.dart @@ -26,7 +26,6 @@ class VocabDeckRepository { }, conflictAlgorithm: ConflictAlgorithm.replace); } - Future loadApiKey() async { String? envApiKey; try { diff --git a/lib/src/themes.dart b/lib/src/themes.dart index 7092f68..a759754 100644 --- a/lib/src/themes.dart +++ b/lib/src/themes.dart @@ -38,7 +38,8 @@ extension CustomTheme on ThemeData { level8: Color(0xFF4FC3F7), // light blue level9: Color(0xFF7986CB), // indigo ); - } else if (colorScheme.primary == const Color(0xFF7B6D53)) { // Nier theme + } else if (colorScheme.primary == const Color(0xFF7B6D53)) { + // Nier theme return const SrsColors( level1: Color(0xFFB71C1C), // dark red level2: Color(0xFFD84315), // deep orange @@ -50,7 +51,8 @@ extension CustomTheme on ThemeData { level8: Color(0xFF0277BD), // light blue level9: Color(0xFF283593), // indigo ); - } else { // Light theme + } else { + // Light theme return const SrsColors( level1: Colors.red, level2: Colors.orange, diff --git a/lib/src/widgets/kanji_card.dart b/lib/src/widgets/kanji_card.dart index eb736e9..9e8bffd 100644 --- a/lib/src/widgets/kanji_card.dart +++ b/lib/src/widgets/kanji_card.dart @@ -19,13 +19,19 @@ class KanjiCard extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final bgColor = backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface; - final fgColor = textColor ?? theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurface; + final bgColor = + backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface; + final fgColor = + textColor ?? + theme.textTheme.bodyMedium?.color ?? + theme.colorScheme.onSurface; return Card( elevation: theme.cardTheme.elevation ?? 12, color: bgColor, - shape: theme.cardTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: + theme.cardTheme.shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: SizedBox( width: 360, height: 240, diff --git a/lib/src/widgets/options_grid.dart b/lib/src/widgets/options_grid.dart index d12fdd6..6c53cf4 100644 --- a/lib/src/widgets/options_grid.dart +++ b/lib/src/widgets/options_grid.dart @@ -39,7 +39,11 @@ class OptionsGrid extends StatelessWidget { Color currentTextColor = fg; if (showResult) { - if (correctAnswers != null && correctAnswers!.contains(o)) { + final normalizedOption = o.trim().toLowerCase(); + if (correctAnswers != null && + correctAnswers! + .map((e) => e.trim().toLowerCase()) + .contains(normalizedOption)) { currentButtonColor = theme.colorScheme.tertiary; } } @@ -51,6 +55,8 @@ class OptionsGrid extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: currentButtonColor, foregroundColor: currentTextColor, + disabledBackgroundColor: + theme.colorScheme.surfaceContainerHighest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -58,7 +64,9 @@ class OptionsGrid extends StatelessWidget { ), child: Text( o, - style: theme.textTheme.titleMedium?.copyWith(color: currentTextColor), + style: theme.textTheme.titleMedium?.copyWith( + color: currentTextColor, + ), textAlign: TextAlign.center, ), ), @@ -66,4 +74,4 @@ class OptionsGrid extends StatelessWidget { }).toList(), ); } -} \ No newline at end of file +}