possible to exclude levels and change how questions are served
This commit is contained in:
@@ -21,8 +21,7 @@ class _QuizState {
|
||||
Key key = UniqueKey();
|
||||
String? selectedOption;
|
||||
bool showResult = false;
|
||||
List<VocabularyItem> shuffledDeck = [];
|
||||
int currentIndex = 0;
|
||||
Set<int> wrongItems = {};
|
||||
}
|
||||
|
||||
class VocabScreen extends StatefulWidget {
|
||||
@@ -40,10 +39,13 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
bool _isAnswering = false;
|
||||
String _status = 'Loading deck...';
|
||||
final DistractorGenerator _dg = DistractorGenerator();
|
||||
final Random _random = Random();
|
||||
final _audioPlayer = AudioPlayer();
|
||||
|
||||
final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
|
||||
_QuizState get _currentQuizState => _quizStates[_tabController.index];
|
||||
final _sessionDecks = <int, List<VocabularyItem>>{};
|
||||
final _sessionDeckSizes = <int, int>{};
|
||||
|
||||
bool _playIncorrectSound = true;
|
||||
bool _playCorrectSound = true;
|
||||
@@ -114,6 +116,40 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
_apiKeyMissing = false;
|
||||
});
|
||||
|
||||
final disabledLevels = <int>{};
|
||||
final itemsByLevel = <int, List<VocabularyItem>>{};
|
||||
for (final item in _deck) {
|
||||
(itemsByLevel[item.level] ??= []).add(item);
|
||||
}
|
||||
|
||||
itemsByLevel.forEach((level, items) {
|
||||
final allSrsItems = items.expand((item) => item.srsItems.values).toList();
|
||||
if (allSrsItems.isNotEmpty && allSrsItems.every((srs) => srs.disabled)) {
|
||||
disabledLevels.add(level);
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < _tabController.length; i++) {
|
||||
final mode = _modeForIndex(i);
|
||||
var filteredDeck = _deck.where((item) {
|
||||
if (disabledLevels.contains(item.level)) {
|
||||
return false;
|
||||
}
|
||||
final srsItem = item.srsItems[mode.toString()];
|
||||
return srsItem == null || !srsItem.disabled;
|
||||
}).toList();
|
||||
|
||||
if (mode == QuizMode.audioToEnglish) {
|
||||
filteredDeck = filteredDeck
|
||||
.where((item) => item.pronunciationAudios.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
filteredDeck.shuffle(_random);
|
||||
_sessionDecks[i] = filteredDeck;
|
||||
_sessionDeckSizes[i] = filteredDeck.length;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _tabController.length; i++) {
|
||||
_nextQuestion(i);
|
||||
}
|
||||
@@ -147,47 +183,20 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
}
|
||||
|
||||
void _nextQuestion([int? index]) {
|
||||
if (_deck.isEmpty) return;
|
||||
final tabIndex = index ?? _tabController.index;
|
||||
final quizState = _quizStates[tabIndex];
|
||||
final sessionDeck = _sessionDecks[tabIndex];
|
||||
final mode = _modeForIndex(tabIndex);
|
||||
|
||||
final quizState = _quizStates[index ?? _tabController.index];
|
||||
final mode = _modeForIndex(index ?? _tabController.index);
|
||||
|
||||
List<VocabularyItem> currentDeckForMode = _deck;
|
||||
if (mode == QuizMode.audioToEnglish) {
|
||||
currentDeckForMode = _deck
|
||||
.where((item) => item.pronunciationAudios.isNotEmpty)
|
||||
.toList();
|
||||
if (currentDeckForMode.isEmpty) {
|
||||
setState(() {
|
||||
_status = 'No vocabulary with audio found.';
|
||||
quizState.current = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (quizState.shuffledDeck.isEmpty ||
|
||||
quizState.currentIndex >= quizState.shuffledDeck.length) {
|
||||
quizState.shuffledDeck = currentDeckForMode.toList();
|
||||
quizState.shuffledDeck.sort((a, b) {
|
||||
final aSrsItem =
|
||||
a.srsItems[mode.toString()] ??
|
||||
SrsItem(subjectId: a.id, quizMode: mode);
|
||||
final bSrsItem =
|
||||
b.srsItems[mode.toString()] ??
|
||||
SrsItem(subjectId: b.id, quizMode: mode);
|
||||
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
|
||||
if (stageComparison != 0) {
|
||||
return stageComparison;
|
||||
}
|
||||
return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked);
|
||||
if (sessionDeck == null || sessionDeck.isEmpty) {
|
||||
setState(() {
|
||||
quizState.current = null;
|
||||
_status = 'Quiz complete!';
|
||||
});
|
||||
quizState.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
quizState.current = quizState.shuffledDeck[quizState.currentIndex];
|
||||
quizState.currentIndex++;
|
||||
|
||||
quizState.current = sessionDeck.removeAt(0);
|
||||
quizState.key = UniqueKey();
|
||||
quizState.correctAnswers = [];
|
||||
quizState.options = [];
|
||||
@@ -218,6 +227,10 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
setState(() {
|
||||
_isAnswering = false;
|
||||
});
|
||||
|
||||
if (mode == QuizMode.audioToEnglish) {
|
||||
_playCurrentAudio(playOnLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _playCurrentAudio({bool playOnLoad = false}) async {
|
||||
@@ -247,6 +260,8 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
|
||||
final repo = Provider.of<VocabDeckRepository>(context, listen: false);
|
||||
final current = quizState.current!;
|
||||
final tabIndex = _tabController.index;
|
||||
final sessionDeck = _sessionDecks[tabIndex]!;
|
||||
|
||||
final srsKey = mode.toString();
|
||||
|
||||
@@ -255,16 +270,21 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
final srsItem =
|
||||
srsItemNullable ?? SrsItem(subjectId: current.id, quizMode: mode);
|
||||
|
||||
quizState.asked += 1;
|
||||
quizState.selectedOption = option;
|
||||
quizState.showResult = true;
|
||||
setState(() {});
|
||||
|
||||
if (isCorrect) {
|
||||
quizState.score += 1;
|
||||
quizState.asked += 1;
|
||||
if (!quizState.wrongItems.contains(current.id)) {
|
||||
quizState.score += 1;
|
||||
}
|
||||
srsItem.srsStage += 1;
|
||||
} else {
|
||||
srsItem.srsStage = max(0, srsItem.srsStage - 1);
|
||||
sessionDeck.add(current);
|
||||
sessionDeck.shuffle(_random);
|
||||
quizState.wrongItems.add(current.id);
|
||||
if (_playIncorrectSound) {
|
||||
await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
|
||||
}
|
||||
@@ -325,7 +345,11 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
_isAnswering = true;
|
||||
});
|
||||
|
||||
_nextQuestion();
|
||||
Future.delayed(const Duration(milliseconds: 900), () {
|
||||
if (mounted) {
|
||||
_nextQuestion();
|
||||
}
|
||||
});;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -360,6 +384,23 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
);
|
||||
}
|
||||
|
||||
if (_loading) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Vocabulary Quiz'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: 'Vocab→English'),
|
||||
Tab(text: 'English→Vocab'),
|
||||
Tab(text: 'Listening'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Vocabulary Quiz'),
|
||||
@@ -383,6 +424,16 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
final quizState = _quizStates[index];
|
||||
final mode = _modeForIndex(index);
|
||||
|
||||
if (quizState.current == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_status,
|
||||
style: TextStyle(
|
||||
fontSize: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget promptWidget;
|
||||
|
||||
if (quizState.current == null) {
|
||||
@@ -424,20 +475,27 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_status,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Text(
|
||||
'${quizState.asked} / ${_sessionDeckSizes[index] ?? 0}',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: (_sessionDeckSizes[index] ?? 0) > 0
|
||||
? quizState.asked / (_sessionDeckSizes[index] ?? 1)
|
||||
: 0,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
@@ -462,10 +520,9 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
OptionsGrid(
|
||||
options: quizState.options,
|
||||
onSelected: _isAnswering ? (option) {} : _answer,
|
||||
isDisabled: false,
|
||||
selectedOption: null,
|
||||
correctAnswers: [],
|
||||
showResult: false,
|
||||
showResult: quizState.showResult,
|
||||
selectedOption: quizState.selectedOption,
|
||||
correctAnswers: quizState.correctAnswers,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user