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();
}
enum VocabQuizMode { vocabToEnglish, englishToVocab }
enum VocabQuizMode { vocabToEnglish, englishToVocab, audioToEnglish }
class VocabSrsItem {
final int vocabId;

View File

@@ -104,7 +104,19 @@ class _VocabScreenState extends State<VocabScreen> {
void _nextQuestion() {
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()] ??
VocabSrsItem(vocabId: a.id, quizMode: _mode);
final bSrsItem = b.srsItems[_mode.toString()] ??
@@ -117,13 +129,17 @@ class _VocabScreenState extends State<VocabScreen> {
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<VocabScreen> {
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 {
final isCorrect = _correctAnswers
.map((a) => a.toLowerCase().trim())
@@ -200,7 +229,7 @@ class _VocabScreenState extends State<VocabScreen> {
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<VocabScreen> {
@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<VocabScreen> {
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<VocabScreen> {
minHeight: 150,
),
child: KanjiCard(
characters: prompt,
characterWidget: promptWidget,
subtitle: '',
backgroundColor: const Color(0xFF1E1E1E),
textColor: Colors.white,
@@ -326,6 +370,7 @@ class _VocabScreenState extends State<VocabScreen> {
),
),
);
}
ChoiceChip _buildChoiceChip(String label, VocabQuizMode mode) {

View File

@@ -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,
),