add new listening comprehension mode #4

Merged
Crylia merged 1 commits from listening_comprehension_mode into master 2025-10-28 20:33:33 +01:00
3 changed files with 71 additions and 23 deletions

View File

@@ -84,7 +84,7 @@ String _katakanaToHiragana(String input) {
return buf.toString(); return buf.toString();
} }
enum VocabQuizMode { vocabToEnglish, englishToVocab } enum VocabQuizMode { vocabToEnglish, englishToVocab, audioToEnglish }
class VocabSrsItem { class VocabSrsItem {
final int vocabId; final int vocabId;

View File

@@ -104,7 +104,19 @@ class _VocabScreenState extends State<VocabScreen> {
void _nextQuestion() { void _nextQuestion() {
if (_deck.isEmpty) return; if (_deck.isEmpty) return;
_deck.sort((a, b) { List<VocabularyItem> 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()] ?? final aSrsItem = a.srsItems[_mode.toString()] ??
VocabSrsItem(vocabId: a.id, quizMode: _mode); VocabSrsItem(vocabId: a.id, quizMode: _mode);
final bSrsItem = b.srsItems[_mode.toString()] ?? final bSrsItem = b.srsItems[_mode.toString()] ??
@@ -117,13 +129,17 @@ class _VocabScreenState extends State<VocabScreen> {
return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked); return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked);
}); });
_current = _deck.first; _current = deck.first;
if (_mode == VocabQuizMode.audioToEnglish) {
_playCurrentAudio();
}
_correctAnswers = []; _correctAnswers = [];
_options = []; _options = [];
switch (_mode) { switch (_mode) {
case VocabQuizMode.vocabToEnglish: case VocabQuizMode.vocabToEnglish:
case VocabQuizMode.audioToEnglish:
_correctAnswers = [_current!.meanings.first]; _correctAnswers = [_current!.meanings.first];
_options = [ _options = [
_correctAnswers.first, _correctAnswers.first,
@@ -144,6 +160,19 @@ class _VocabScreenState extends State<VocabScreen> {
setState(() {}); setState(() {});
} }
Future<void> _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 { void _answer(String option) async {
final isCorrect = _correctAnswers final isCorrect = _correctAnswers
.map((a) => a.toLowerCase().trim()) .map((a) => a.toLowerCase().trim())
@@ -200,7 +229,7 @@ class _VocabScreenState extends State<VocabScreen> {
if (_playCorrectSound) { if (_playCorrectSound) {
await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
} }
if (_playAudio) { if (_playAudio && _mode != VocabQuizMode.audioToEnglish) {
final maleAudios = final maleAudios =
current.pronunciationAudios.where((a) => a.gender == 'male'); current.pronunciationAudios.where((a) => a.gender == 'male');
if (maleAudios.isNotEmpty) { if (maleAudios.isNotEmpty) {
@@ -228,15 +257,29 @@ class _VocabScreenState extends State<VocabScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String prompt = ''; Widget promptWidget;
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) { switch (_mode) {
case VocabQuizMode.vocabToEnglish: case VocabQuizMode.vocabToEnglish:
prompt = _current?.characters ?? ''; promptText = _current?.characters ?? '';
break; break;
case VocabQuizMode.englishToVocab: case VocabQuizMode.englishToVocab:
prompt = _current != null ? _toTitleCase(_current!.meanings.first) : ''; promptText = _current != null ? _toTitleCase(_current!.meanings.first) : '';
break; break;
case VocabQuizMode.audioToEnglish:
// Handled above
break;
}
promptWidget = Text(promptText, style: const TextStyle(fontSize: 48, color: Colors.white));
} }
return Scaffold( return Scaffold(
@@ -282,6 +325,7 @@ class _VocabScreenState extends State<VocabScreen> {
children: [ children: [
_buildChoiceChip('Vocab→English', VocabQuizMode.vocabToEnglish), _buildChoiceChip('Vocab→English', VocabQuizMode.vocabToEnglish),
_buildChoiceChip('English→Vocab', VocabQuizMode.englishToVocab), _buildChoiceChip('English→Vocab', VocabQuizMode.englishToVocab),
_buildChoiceChip('Listening', VocabQuizMode.audioToEnglish),
], ],
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
@@ -295,7 +339,7 @@ class _VocabScreenState extends State<VocabScreen> {
minHeight: 150, minHeight: 150,
), ),
child: KanjiCard( child: KanjiCard(
characters: prompt, characterWidget: promptWidget,
subtitle: '', subtitle: '',
backgroundColor: const Color(0xFF1E1E1E), backgroundColor: const Color(0xFF1E1E1E),
textColor: Colors.white, textColor: Colors.white,
@@ -326,6 +370,7 @@ class _VocabScreenState extends State<VocabScreen> {
), ),
), ),
); );
} }
ChoiceChip _buildChoiceChip(String label, VocabQuizMode mode) { ChoiceChip _buildChoiceChip(String label, VocabQuizMode mode) {

View File

@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
class KanjiCard extends StatelessWidget { class KanjiCard extends StatelessWidget {
final String characters; final String characters;
final Widget? characterWidget;
final String subtitle; final String subtitle;
final Color? backgroundColor; final Color? backgroundColor;
final Color? textColor; final Color? textColor;
const KanjiCard({ const KanjiCard({
super.key, super.key,
required this.characters, this.characters = '',
this.characterWidget,
this.subtitle = '', this.subtitle = '',
this.backgroundColor, this.backgroundColor,
this.textColor, this.textColor,
@@ -32,6 +34,7 @@ class KanjiCard extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
characterWidget ??
Text( Text(
characters, characters,
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
@@ -44,7 +47,7 @@ class KanjiCard extends StatelessWidget {
Text( Text(
subtitle, subtitle,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: fgColor.withValues(alpha: 0.7), color: fgColor.withAlpha((255 * 0.7).round()),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),