diff --git a/lib/src/models/kanji_item.dart b/lib/src/models/kanji_item.dart index f1bade7..dcbbbb9 100644 --- a/lib/src/models/kanji_item.dart +++ b/lib/src/models/kanji_item.dart @@ -84,7 +84,7 @@ String _katakanaToHiragana(String input) { return buf.toString(); } -enum VocabQuizMode { vocabToEnglish, englishToVocab } +enum VocabQuizMode { vocabToEnglish, englishToVocab, audioToEnglish } class VocabSrsItem { final int vocabId; diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index 7cb3ad3..158e702 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -104,7 +104,19 @@ class _VocabScreenState extends State { void _nextQuestion() { if (_deck.isEmpty) return; - _deck.sort((a, b) { + List deck = _deck; + if (_mode == VocabQuizMode.audioToEnglish) { + deck = _deck.where((item) => item.pronunciationAudios.isNotEmpty).toList(); + if (deck.isEmpty) { + setState(() { + _status = 'No vocabulary with audio found.'; + _current = null; + }); + return; + } + } + + deck.sort((a, b) { final aSrsItem = a.srsItems[_mode.toString()] ?? VocabSrsItem(vocabId: a.id, quizMode: _mode); final bSrsItem = b.srsItems[_mode.toString()] ?? @@ -117,13 +129,17 @@ class _VocabScreenState extends State { return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked); }); - _current = _deck.first; + _current = deck.first; + if (_mode == VocabQuizMode.audioToEnglish) { + _playCurrentAudio(); + } _correctAnswers = []; _options = []; switch (_mode) { case VocabQuizMode.vocabToEnglish: + case VocabQuizMode.audioToEnglish: _correctAnswers = [_current!.meanings.first]; _options = [ _correctAnswers.first, @@ -144,6 +160,19 @@ class _VocabScreenState extends State { setState(() {}); } + Future _playCurrentAudio() async { + 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); + + try { + await _audioPlayer.play(UrlSource(audioUrl)); + } catch (e) { + // Ignore player errors + } + } + void _answer(String option) async { final isCorrect = _correctAnswers .map((a) => a.toLowerCase().trim()) @@ -200,7 +229,7 @@ class _VocabScreenState extends State { if (_playCorrectSound) { await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); } - if (_playAudio) { + if (_playAudio && _mode != VocabQuizMode.audioToEnglish) { final maleAudios = current.pronunciationAudios.where((a) => a.gender == 'male'); if (maleAudios.isNotEmpty) { @@ -228,15 +257,29 @@ class _VocabScreenState extends State { @override Widget build(BuildContext context) { - String prompt = ''; + Widget promptWidget; - switch (_mode) { - case VocabQuizMode.vocabToEnglish: - prompt = _current?.characters ?? ''; - break; - case VocabQuizMode.englishToVocab: - prompt = _current != null ? _toTitleCase(_current!.meanings.first) : ''; - break; + if (_current == null) { + promptWidget = const SizedBox.shrink(); + } else if (_mode == VocabQuizMode.audioToEnglish) { + promptWidget = IconButton( + icon: const Icon(Icons.volume_up, color: Colors.white, size: 64), + onPressed: _playCurrentAudio, + ); + } else { + String promptText = ''; + switch (_mode) { + case VocabQuizMode.vocabToEnglish: + promptText = _current?.characters ?? ''; + break; + case VocabQuizMode.englishToVocab: + promptText = _current != null ? _toTitleCase(_current!.meanings.first) : ''; + break; + case VocabQuizMode.audioToEnglish: + // Handled above + break; + } + promptWidget = Text(promptText, style: const TextStyle(fontSize: 48, color: Colors.white)); } return Scaffold( @@ -282,6 +325,7 @@ class _VocabScreenState extends State { children: [ _buildChoiceChip('Vocab→English', VocabQuizMode.vocabToEnglish), _buildChoiceChip('English→Vocab', VocabQuizMode.englishToVocab), + _buildChoiceChip('Listening', VocabQuizMode.audioToEnglish), ], ), const SizedBox(height: 18), @@ -295,7 +339,7 @@ class _VocabScreenState extends State { minHeight: 150, ), child: KanjiCard( - characters: prompt, + characterWidget: promptWidget, subtitle: '', backgroundColor: const Color(0xFF1E1E1E), textColor: Colors.white, @@ -326,6 +370,7 @@ class _VocabScreenState extends State { ), ), ); + } ChoiceChip _buildChoiceChip(String label, VocabQuizMode mode) { diff --git a/lib/src/widgets/kanji_card.dart b/lib/src/widgets/kanji_card.dart index 008b272..eb736e9 100644 --- a/lib/src/widgets/kanji_card.dart +++ b/lib/src/widgets/kanji_card.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; class KanjiCard extends StatelessWidget { final String characters; + final Widget? characterWidget; final String subtitle; final Color? backgroundColor; final Color? textColor; const KanjiCard({ super.key, - required this.characters, + this.characters = '', + this.characterWidget, this.subtitle = '', this.backgroundColor, this.textColor, @@ -32,19 +34,20 @@ class KanjiCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - characters, - style: theme.textTheme.headlineMedium?.copyWith( - fontSize: 56, - color: fgColor, - ), - textAlign: TextAlign.center, - ), + characterWidget ?? + Text( + characters, + style: theme.textTheme.headlineMedium?.copyWith( + fontSize: 56, + color: fgColor, + ), + textAlign: TextAlign.center, + ), const SizedBox(height: 8), Text( subtitle, style: theme.textTheme.bodyMedium?.copyWith( - color: fgColor.withValues(alpha: 0.7), + color: fgColor.withAlpha((255 * 0.7).round()), ), textAlign: TextAlign.center, ),