This commit is contained in:
Rene Kievits
2025-10-31 13:40:14 +01:00
parent ad61292263
commit 4eb488e28c
16 changed files with 691 additions and 395 deletions

View File

@@ -31,7 +31,8 @@ class VocabScreen extends StatefulWidget {
State<VocabScreen> createState() => _VocabScreenState();
}
class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStateMixin {
class _VocabScreenState extends State<VocabScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<VocabularyItem> _deck = [];
bool _loading = false;
@@ -150,7 +151,9 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
List<VocabularyItem> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<void>();
final sub = _audioPlayer.onPlayerComplete.listen((event) {
@@ -300,19 +307,15 @@ class _VocabScreenState extends State<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> 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<VocabScreen> with SingleTickerProviderStat
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characterWidget: promptWidget,
subtitle: '',
),
child: KanjiCard(characterWidget: promptWidget, subtitle: ''),
),
),
),
@@ -445,7 +440,7 @@ class _VocabScreenState extends State<VocabScreen> 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<VocabScreen> with SingleTickerProviderStat
),
);
}
}
}