possible to exclude levels and change how questions are served

This commit is contained in:
Rene Kievits
2025-11-02 17:07:23 +01:00
parent e9f115a32a
commit 16da0f04ac
10 changed files with 477 additions and 175 deletions

View File

@@ -6,6 +6,7 @@ class SrsItem {
final String? readingType; final String? readingType;
int srsStage; int srsStage;
DateTime lastAsked; DateTime lastAsked;
bool disabled;
SrsItem({ SrsItem({
required this.subjectId, required this.subjectId,
@@ -13,5 +14,6 @@ class SrsItem {
this.readingType, this.readingType,
this.srsStage = 0, this.srsStage = 0,
DateTime? lastAsked, DateTime? lastAsked,
this.disabled = false,
}) : lastAsked = lastAsked ?? DateTime.now(); }) : lastAsked = lastAsked ?? DateTime.now();
} }

View File

@@ -123,9 +123,14 @@ class _BrowseScreenState extends State<BrowseScreen>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
CircularProgressIndicator(color: Theme.of(context).colorScheme.primary), CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), Text(
_status,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
], ],
), ),
); );
@@ -151,6 +156,7 @@ class _BrowseScreenState extends State<BrowseScreen>
List<int> sortedLevels, List<int> sortedLevels,
PageController pageController, PageController pageController,
Widget Function(List<dynamic>) buildPageContent, Widget Function(List<dynamic>) buildPageContent,
dynamic repository,
) { ) {
if (sortedLevels.isEmpty) { if (sortedLevels.isEmpty) {
return Center( return Center(
@@ -167,18 +173,34 @@ class _BrowseScreenState extends State<BrowseScreen>
itemBuilder: (context, index) { itemBuilder: (context, index) {
final level = sortedLevels[index]; final level = sortedLevels[index];
final levelItems = groupedItems[level]!; final levelItems = groupedItems[level]!;
final bool isDisabled = levelItems.every(
(item) => (item as dynamic).srsItems.values.every(
(srs) => (srs as SrsItem).disabled,
),
);
return Column( return Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Row(
'Level $level', mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle( children: [
fontSize: 24, Text(
color: Theme.of(context).colorScheme.onSurface, 'Level $level',
fontWeight: FontWeight.bold, style: TextStyle(
), fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Checkbox(
value: !isDisabled,
onChanged: (value) {
_toggleLevelExclusion(level, repository);
},
),
],
), ),
), ),
Expanded(child: buildPageContent(levelItems)), Expanded(child: buildPageContent(levelItems)),
@@ -199,31 +221,55 @@ class _BrowseScreenState extends State<BrowseScreen>
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
height: 60,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(levels.length, (index) { children: List.generate(levels.length, (index) {
final level = levels[index]; final level = levels[index];
final isSelected = index == currentPage; final isSelected = index == currentPage;
final items = isKanji ? _kanjiByLevel[level] : _vocabByLevel[level];
final bool isDisabled =
items?.every(
(item) => (item as dynamic).srsItems.values.every(
(srs) => (srs as SrsItem).disabled,
),
) ??
false;
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
controller.animateToPage( controller.animateToPage(
index, index,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isSelected backgroundColor: isSelected
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: isDisabled
? Theme.of(context).colorScheme.surfaceVariant
: Theme.of(context).colorScheme.surfaceContainerHighest, : Theme.of(context).colorScheme.surfaceContainerHighest,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), foregroundColor: isSelected
? Theme.of(context).colorScheme.onPrimary
: isDisabled
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
), ),
child: Text(level.toString()), child: Text(level.toString()),
), ),
), ),
@@ -295,7 +341,10 @@ class _BrowseScreenState extends State<BrowseScreen>
Expanded( Expanded(
child: Text( child: Text(
item.characters, item.characters,
style: TextStyle(fontSize: 24, color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@@ -306,7 +355,9 @@ class _BrowseScreenState extends State<BrowseScreen>
children: [ children: [
Text( Text(
item.meanings.join(', '), item.meanings.join(', '),
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -355,7 +406,10 @@ class _BrowseScreenState extends State<BrowseScreen>
children: [ children: [
Text( Text(
item.characters, item.characters,
style: TextStyle(fontSize: 32, color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
fontSize: 32,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -375,7 +429,9 @@ class _BrowseScreenState extends State<BrowseScreen>
height: 10, height: 10,
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: level / 9.0, value: level / 9.0,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(
_getColorForSrsLevel(level), _getColorForSrsLevel(level),
), ),
@@ -444,29 +500,39 @@ class _BrowseScreenState extends State<BrowseScreen>
children: [ children: [
Text( Text(
'Level: ${kanji.level}', 'Level: ${kanji.level}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (kanji.meanings.isNotEmpty) if (kanji.meanings.isNotEmpty)
Text( Text(
'Meanings: ${kanji.meanings.join(', ')}', 'Meanings: ${kanji.meanings.join(', ')}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (kanji.onyomi.isNotEmpty) if (kanji.onyomi.isNotEmpty)
Text( Text(
'On\'yomi: ${kanji.onyomi.join(', ')}', 'On\'yomi: ${kanji.onyomi.join(', ')}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
if (kanji.kunyomi.isNotEmpty) if (kanji.kunyomi.isNotEmpty)
Text( Text(
'Kun\'yomi: ${kanji.kunyomi.join(', ')}', 'Kun\'yomi: ${kanji.kunyomi.join(', ')}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty) if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty)
Text( Text(
'No readings available.', 'No readings available.',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), Divider(color: Theme.of(context).colorScheme.onSurfaceVariant),
@@ -481,7 +547,9 @@ class _BrowseScreenState extends State<BrowseScreen>
...srsScores.entries.map( ...srsScores.entries.map(
(entry) => Text( (entry) => Text(
' ${entry.key}: ${entry.value}', ' ${entry.key}: ${entry.value}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
), ),
], ],
@@ -568,6 +636,42 @@ class _BrowseScreenState extends State<BrowseScreen>
_vocabSortedLevels = _vocabByLevel.keys.toList()..sort(); _vocabSortedLevels = _vocabByLevel.keys.toList()..sort();
} }
Future<void> _toggleLevelExclusion(int level, dynamic repository) async {
final List<SrsItem> itemsToUpdate = [];
final bool currentlyDisabled;
if (repository is DeckRepository) {
final items = _kanjiByLevel[level] ?? [];
currentlyDisabled = items.every(
(item) => item.srsItems.values.every((srs) => srs.disabled),
);
for (final item in items) {
for (final srsItem in item.srsItems.values) {
itemsToUpdate.add(srsItem);
}
}
} else if (repository is VocabDeckRepository) {
final items = _vocabByLevel[level] ?? [];
currentlyDisabled = items.every(
(item) => item.srsItems.values.every((srs) => srs.disabled),
);
for (final item in items) {
for (final srsItem in item.srsItems.values) {
itemsToUpdate.add(srsItem);
}
}
} else {
return;
}
for (final item in itemsToUpdate) {
item.disabled = !currentlyDisabled;
}
await repository.updateSrsItems(itemsToUpdate);
_loadDecks();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -586,6 +690,7 @@ class _BrowseScreenState extends State<BrowseScreen>
_kanjiSortedLevels, _kanjiSortedLevels,
_kanjiPageController, _kanjiPageController,
(items) => _buildGridView(items.cast<KanjiItem>()), (items) => _buildGridView(items.cast<KanjiItem>()),
Provider.of<DeckRepository>(context, listen: false),
), ),
), ),
_buildWaniKaniTab( _buildWaniKaniTab(
@@ -594,6 +699,7 @@ class _BrowseScreenState extends State<BrowseScreen>
_vocabSortedLevels, _vocabSortedLevels,
_vocabPageController, _vocabPageController,
(items) => _buildListView(items.cast<VocabularyItem>()), (items) => _buildListView(items.cast<VocabularyItem>()),
Provider.of<VocabDeckRepository>(context, listen: false),
), ),
), ),
_buildCustomSrsTab(), _buildCustomSrsTab(),
@@ -673,8 +779,7 @@ class _BrowseScreenState extends State<BrowseScreen>
setState(() { setState(() {
if (_selectedItems.length == _customDeck.length) { if (_selectedItems.length == _customDeck.length) {
_selectedItems.clear(); _selectedItems.clear();
} } else {
else {
_selectedItems = List.from(_customDeck); _selectedItems = List.from(_customDeck);
} }
}); });
@@ -799,12 +904,17 @@ class _BrowseScreenState extends State<BrowseScreen>
child: Card( child: Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: isSelected side: isSelected
? BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0) ? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.0,
)
: BorderSide.none, : BorderSide.none,
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
), ),
color: isSelected color: isSelected
? Theme.of(context).colorScheme.primary.withAlpha((255 * 0.5).round()) ? Theme.of(
context,
).colorScheme.primary.withAlpha((255 * 0.5).round())
: Theme.of(context).colorScheme.surfaceContainer, : Theme.of(context).colorScheme.surfaceContainer,
child: Stack( child: Stack(
children: [ children: [
@@ -854,7 +964,11 @@ class _BrowseScreenState extends State<BrowseScreen>
Positioned( Positioned(
top: 4, top: 4,
right: 4, right: 4,
child: Icon(Icons.timer, color: Theme.of(context).colorScheme.tertiary, size: 16), child: Icon(
Icons.timer,
color: Theme.of(context).colorScheme.tertiary,
size: 16,
),
), ),
], ],
), ),
@@ -912,11 +1026,15 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
children: [ children: [
Text( Text(
japaneseWord, japaneseWord,
style: TextStyle(color: widget.theme.colorScheme.onSurface), style: TextStyle(
color: widget.theme.colorScheme.onSurface,
),
), ),
Text( Text(
englishDefinition, englishDefinition,
style: TextStyle(color: widget.theme.colorScheme.onSurfaceVariant), style: TextStyle(
color: widget.theme.colorScheme.onSurfaceVariant,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@@ -1012,7 +1130,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'SRS Scores:', 'SRS Scores:',
style: TextStyle(color: widget.theme.colorScheme.onSurface, fontWeight: FontWeight.bold), style: TextStyle(
color: widget.theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
), ),
...srsScores.entries.map( ...srsScores.entries.map(
(entry) => Text( (entry) => Text(
@@ -1025,7 +1146,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Example Sentences:', 'Example Sentences:',
style: TextStyle(color: widget.theme.colorScheme.onSurface, fontWeight: FontWeight.bold), style: TextStyle(
color: widget.theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
), ),
..._exampleSentences, ..._exampleSentences,
], ],
@@ -1059,4 +1183,3 @@ void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
}, },
); );
} }

View File

@@ -50,6 +50,10 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
bool _playCorrectSound = true; bool _playCorrectSound = true;
bool _playNarrator = true; bool _playNarrator = true;
String? _selectedOption;
String? _correctAnswer;
bool _showResult = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -148,6 +152,9 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
); );
} }
} }
while (_options.length < 4) {
_options.add('---');
}
_options.shuffle(); _options.shuffle();
} }
@@ -160,6 +167,12 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
: currentItem.meaning; : currentItem.meaning;
final isCorrect = answer == correctAnswer; final isCorrect = answer == correctAnswer;
setState(() {
_selectedOption = answer;
_correctAnswer = correctAnswer;
_showResult = true;
});
int currentSrsLevel = 0; // Initialize with a default value int currentSrsLevel = 0; // Initialize with a default value
if (currentItem.useInterval) { if (currentItem.useInterval) {
switch (widget.quizMode) { switch (widget.quizMode) {
@@ -239,7 +252,9 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
content: Text( content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle( style: TextStyle(
color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error, color: isCorrect
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -312,6 +327,9 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
_currentIndex++; _currentIndex++;
_answered = false; _answered = false;
_correct = null; _correct = null;
_selectedOption = null;
_correctAnswer = null;
_showResult = false;
if (_currentIndex < _shuffledDeck.length) { if (_currentIndex < _shuffledDeck.length) {
_generateOptions(); _generateOptions();
} }

View File

@@ -29,6 +29,7 @@ class _QuizState {
Key key = UniqueKey(); Key key = UniqueKey();
String? selectedOption; String? selectedOption;
bool showResult = false; bool showResult = false;
Set<int> wrongItems = {};
} }
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -52,6 +53,8 @@ class _HomeScreenState extends State<HomeScreen>
final _audioPlayer = AudioPlayer(); final _audioPlayer = AudioPlayer();
final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
final _sessionDecks = <int, List<KanjiItem>>{};
final _sessionDeckSizes = <int, int>{};
_QuizState get _currentQuizState => _quizStates[_tabController.index]; _QuizState get _currentQuizState => _quizStates[_tabController.index];
bool _playIncorrectSound = true; bool _playIncorrectSound = true;
@@ -118,6 +121,44 @@ class _HomeScreenState extends State<HomeScreen>
_apiKeyMissing = false; _apiKeyMissing = false;
}); });
final disabledLevels = <int>{};
final itemsByLevel = <int, List<KanjiItem>>{};
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);
final filteredDeck = _deck.where((item) {
if (disabledLevels.contains(item.level)) {
return false;
}
if (mode == QuizMode.reading) {
final onyomiSrs = item.srsItems['${QuizMode.reading}onyomi'];
final kunyomiSrs = item.srsItems['${QuizMode.reading}kunyomi'];
final hasOnyomi = item.onyomi.isNotEmpty &&
(onyomiSrs == null || !onyomiSrs.disabled);
final hasKunyomi = item.kunyomi.isNotEmpty &&
(kunyomiSrs == null || !kunyomiSrs.disabled);
return hasOnyomi || hasKunyomi;
}
final srsItem = item.srsItems[mode.toString()];
return srsItem == null || !srsItem.disabled;
}).toList();
filteredDeck.shuffle(_random);
_sessionDecks[i] = filteredDeck;
_sessionDeckSizes[i] = filteredDeck.length;
}
for (var i = 0; i < _tabController.length; i++) { for (var i = 0; i < _tabController.length; i++) {
_nextQuestion(i); _nextQuestion(i);
} }
@@ -164,61 +205,20 @@ class _HomeScreenState extends State<HomeScreen>
} }
void _nextQuestion([int? index]) { 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]; if (sessionDeck == null || sessionDeck.isEmpty) {
final mode = _modeForIndex(index ?? _tabController.index); setState(() {
quizState.current = null;
_status = 'Quiz complete!';
});
return;
}
_deck.sort((a, b) { quizState.current = sessionDeck.removeAt(0);
int getSrsStage(KanjiItem item) {
if (mode == QuizMode.reading) {
final onyomiStage =
item.srsItems['${QuizMode.reading}onyomi']?.srsStage;
final kunyomiStage =
item.srsItems['${QuizMode.reading}kunyomi']?.srsStage;
if (onyomiStage != null && kunyomiStage != null) {
return min(onyomiStage, kunyomiStage);
}
return onyomiStage ?? kunyomiStage ?? 0;
}
return item.srsItems[mode.toString()]?.srsStage ?? 0;
}
DateTime getLastAsked(KanjiItem item) {
if (mode == QuizMode.reading) {
final onyomiLastAsked =
item.srsItems['${QuizMode.reading}onyomi']?.lastAsked;
final kunyomiLastAsked =
item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked;
if (onyomiLastAsked != null && kunyomiLastAsked != null) {
return onyomiLastAsked.isBefore(kunyomiLastAsked)
? onyomiLastAsked
: kunyomiLastAsked;
}
return onyomiLastAsked ??
kunyomiLastAsked ??
DateTime.fromMillisecondsSinceEpoch(0);
}
return item.srsItems[mode.toString()]?.lastAsked ??
DateTime.fromMillisecondsSinceEpoch(0);
}
final aStage = getSrsStage(a);
final bStage = getSrsStage(b);
if (aStage != bStage) {
return aStage.compareTo(bStage);
}
final aLastAsked = getLastAsked(a);
final bLastAsked = getLastAsked(b);
return aLastAsked.compareTo(bLastAsked);
});
quizState.current = _deck.first;
quizState.key = UniqueKey(); quizState.key = UniqueKey();
quizState.correctAnswers = []; quizState.correctAnswers = [];
@@ -284,12 +284,13 @@ class _HomeScreenState extends State<HomeScreen>
final repo = Provider.of<DeckRepository>(context, listen: false); final repo = Provider.of<DeckRepository>(context, listen: false);
final current = quizState.current!; final current = quizState.current!;
final tabIndex = _tabController.index;
final sessionDeck = _sessionDecks[tabIndex]!;
String readingType = ''; String readingType = '';
if (mode == QuizMode.reading) { if (mode == QuizMode.reading) {
readingType = quizState.readingHint.contains("on'yomi") readingType =
? 'onyomi' quizState.readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
: 'kunyomi';
} }
final srsKey = mode.toString() + readingType; final srsKey = mode.toString() + readingType;
@@ -301,8 +302,6 @@ class _HomeScreenState extends State<HomeScreen>
readingType: readingType, readingType: readingType,
); );
quizState.asked += 1;
quizState.selectedOption = option; quizState.selectedOption = option;
quizState.showResult = true; quizState.showResult = true;
@@ -310,13 +309,19 @@ class _HomeScreenState extends State<HomeScreen>
setState(() {}); setState(() {});
if (isCorrect) { if (isCorrect) {
quizState.score += 1; quizState.asked += 1;
if (!quizState.wrongItems.contains(current.id)) {
quizState.score += 1;
}
srsItemForUpdate.srsStage += 1; srsItemForUpdate.srsStage += 1;
if (_playCorrectSound) { if (_playCorrectSound) {
_audioPlayer.play(AssetSource('sfx/correct.wav')); _audioPlayer.play(AssetSource('sfx/correct.wav'));
} }
} else { } else {
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1); srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
sessionDeck.add(current);
sessionDeck.shuffle(_random);
quizState.wrongItems.add(current.id);
if (_playIncorrectSound) { if (_playIncorrectSound) {
_audioPlayer.play(AssetSource('sfx/incorrect.wav')); _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
} }
@@ -336,8 +341,8 @@ class _HomeScreenState extends State<HomeScreen>
final correctDisplay = (mode == QuizMode.kanjiToEnglish) final correctDisplay = (mode == QuizMode.kanjiToEnglish)
? _toTitleCase(quizState.correctAnswers.first) ? _toTitleCase(quizState.correctAnswers.first)
: (mode == QuizMode.reading : (mode == QuizMode.reading
? quizState.correctAnswers.join(', ') ? quizState.correctAnswers.join(', ')
: quizState.correctAnswers.first); : quizState.correctAnswers.first);
final snack = SnackBar( final snack = SnackBar(
content: Text( content: Text(
@@ -398,6 +403,23 @@ class _HomeScreenState extends State<HomeScreen>
); );
} }
if (_loading) {
return Scaffold(
appBar: AppBar(
title: const Text('Kanji Quiz'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Kanji→English'),
Tab(text: 'English→Kanji'),
Tab(text: 'Reading'),
],
),
),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Kanji Quiz'), title: const Text('Kanji Quiz'),
@@ -422,6 +444,16 @@ class _HomeScreenState extends State<HomeScreen>
final quizState = _quizStates[index]; final quizState = _quizStates[index];
final mode = _modeForIndex(index); final mode = _modeForIndex(index);
if (quizState.current == null) {
return Center(
child: Text(
_status,
style: TextStyle(
fontSize: 24, color: Theme.of(context).colorScheme.onSurface),
),
);
}
String prompt = ''; String prompt = '';
String subtitle = ''; String subtitle = '';
@@ -447,20 +479,27 @@ class _HomeScreenState extends State<HomeScreen>
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Text(
child: Text( '${quizState.asked} / ${_sessionDeckSizes[index] ?? 0}',
_status, style: TextStyle(
style: TextStyle( color: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).colorScheme.onSurface, fontSize: 18,
), fontWeight: FontWeight.bold,
), ),
), ),
if (_loading) const SizedBox(height: 4),
CircularProgressIndicator( LinearProgressIndicator(
color: Theme.of(context).colorScheme.primary, 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), const SizedBox(height: 18),
@@ -490,10 +529,9 @@ class _HomeScreenState extends State<HomeScreen>
OptionsGrid( OptionsGrid(
options: quizState.options, options: quizState.options,
onSelected: _isAnswering ? (option) {} : _answer, onSelected: _isAnswering ? (option) {} : _answer,
isDisabled: false, showResult: quizState.showResult,
selectedOption: null, selectedOption: quizState.selectedOption,
correctAnswers: [], correctAnswers: quizState.correctAnswers,
showResult: false,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(

View File

@@ -21,8 +21,7 @@ class _QuizState {
Key key = UniqueKey(); Key key = UniqueKey();
String? selectedOption; String? selectedOption;
bool showResult = false; bool showResult = false;
List<VocabularyItem> shuffledDeck = []; Set<int> wrongItems = {};
int currentIndex = 0;
} }
class VocabScreen extends StatefulWidget { class VocabScreen extends StatefulWidget {
@@ -40,10 +39,13 @@ class _VocabScreenState extends State<VocabScreen>
bool _isAnswering = false; bool _isAnswering = false;
String _status = 'Loading deck...'; String _status = 'Loading deck...';
final DistractorGenerator _dg = DistractorGenerator(); final DistractorGenerator _dg = DistractorGenerator();
final Random _random = Random();
final _audioPlayer = AudioPlayer(); final _audioPlayer = AudioPlayer();
final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
_QuizState get _currentQuizState => _quizStates[_tabController.index]; _QuizState get _currentQuizState => _quizStates[_tabController.index];
final _sessionDecks = <int, List<VocabularyItem>>{};
final _sessionDeckSizes = <int, int>{};
bool _playIncorrectSound = true; bool _playIncorrectSound = true;
bool _playCorrectSound = true; bool _playCorrectSound = true;
@@ -114,6 +116,40 @@ class _VocabScreenState extends State<VocabScreen>
_apiKeyMissing = false; _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++) { for (var i = 0; i < _tabController.length; i++) {
_nextQuestion(i); _nextQuestion(i);
} }
@@ -147,47 +183,20 @@ class _VocabScreenState extends State<VocabScreen>
} }
void _nextQuestion([int? index]) { 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]; if (sessionDeck == null || sessionDeck.isEmpty) {
final mode = _modeForIndex(index ?? _tabController.index); setState(() {
quizState.current = null;
List<VocabularyItem> currentDeckForMode = _deck; _status = 'Quiz complete!';
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);
}); });
quizState.currentIndex = 0; return;
} }
quizState.current = quizState.shuffledDeck[quizState.currentIndex]; quizState.current = sessionDeck.removeAt(0);
quizState.currentIndex++;
quizState.key = UniqueKey(); quizState.key = UniqueKey();
quizState.correctAnswers = []; quizState.correctAnswers = [];
quizState.options = []; quizState.options = [];
@@ -218,6 +227,10 @@ class _VocabScreenState extends State<VocabScreen>
setState(() { setState(() {
_isAnswering = false; _isAnswering = false;
}); });
if (mode == QuizMode.audioToEnglish) {
_playCurrentAudio(playOnLoad: true);
}
} }
Future<void> _playCurrentAudio({bool playOnLoad = false}) async { 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 repo = Provider.of<VocabDeckRepository>(context, listen: false);
final current = quizState.current!; final current = quizState.current!;
final tabIndex = _tabController.index;
final sessionDeck = _sessionDecks[tabIndex]!;
final srsKey = mode.toString(); final srsKey = mode.toString();
@@ -255,16 +270,21 @@ class _VocabScreenState extends State<VocabScreen>
final srsItem = final srsItem =
srsItemNullable ?? SrsItem(subjectId: current.id, quizMode: mode); srsItemNullable ?? SrsItem(subjectId: current.id, quizMode: mode);
quizState.asked += 1;
quizState.selectedOption = option; quizState.selectedOption = option;
quizState.showResult = true; quizState.showResult = true;
setState(() {}); setState(() {});
if (isCorrect) { if (isCorrect) {
quizState.score += 1; quizState.asked += 1;
if (!quizState.wrongItems.contains(current.id)) {
quizState.score += 1;
}
srsItem.srsStage += 1; srsItem.srsStage += 1;
} else { } else {
srsItem.srsStage = max(0, srsItem.srsStage - 1); srsItem.srsStage = max(0, srsItem.srsStage - 1);
sessionDeck.add(current);
sessionDeck.shuffle(_random);
quizState.wrongItems.add(current.id);
if (_playIncorrectSound) { if (_playIncorrectSound) {
await _audioPlayer.play(AssetSource('sfx/incorrect.wav')); await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
} }
@@ -325,7 +345,11 @@ class _VocabScreenState extends State<VocabScreen>
_isAnswering = true; _isAnswering = true;
}); });
_nextQuestion(); Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) {
_nextQuestion();
}
});;
} }
@override @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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Vocabulary Quiz'), title: const Text('Vocabulary Quiz'),
@@ -383,6 +424,16 @@ class _VocabScreenState extends State<VocabScreen>
final quizState = _quizStates[index]; final quizState = _quizStates[index];
final mode = _modeForIndex(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; Widget promptWidget;
if (quizState.current == null) { if (quizState.current == null) {
@@ -424,20 +475,27 @@ class _VocabScreenState extends State<VocabScreen>
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Text(
child: Text( '${quizState.asked} / ${_sessionDeckSizes[index] ?? 0}',
_status, style: TextStyle(
style: TextStyle( color: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).colorScheme.onSurface, fontSize: 18,
), fontWeight: FontWeight.bold,
), ),
), ),
if (_loading) const SizedBox(height: 4),
CircularProgressIndicator( LinearProgressIndicator(
color: Theme.of(context).colorScheme.primary, 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), const SizedBox(height: 18),
@@ -462,10 +520,9 @@ class _VocabScreenState extends State<VocabScreen>
OptionsGrid( OptionsGrid(
options: quizState.options, options: quizState.options,
onSelected: _isAnswering ? (option) {} : _answer, onSelected: _isAnswering ? (option) {} : _answer,
isDisabled: false, showResult: quizState.showResult,
selectedOption: null, selectedOption: quizState.selectedOption,
correctAnswers: [], correctAnswers: quizState.correctAnswers,
showResult: false,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(

View File

@@ -24,4 +24,5 @@ class DbConstants {
static const String readingTypeColumn = 'readingType'; static const String readingTypeColumn = 'readingType';
static const String srsStageColumn = 'srsStage'; static const String srsStageColumn = 'srsStage';
static const String lastAskedColumn = 'lastAsked'; static const String lastAskedColumn = 'lastAsked';
static const String disabledColumn = 'disabled';
} }

View File

@@ -32,7 +32,7 @@ class DatabaseHelper {
return openDatabase( return openDatabase(
path, path,
version: 7, version: 8,
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute( await db.execute(
'''CREATE TABLE ${DbConstants.kanjiTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.onyomiColumn} TEXT, ${DbConstants.kunyomiColumn} TEXT)''', '''CREATE TABLE ${DbConstants.kanjiTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.onyomiColumn} TEXT, ${DbConstants.kunyomiColumn} TEXT)''',
@@ -41,15 +41,21 @@ class DatabaseHelper {
'''CREATE TABLE ${DbConstants.settingsTable} (${DbConstants.keyColumn} TEXT PRIMARY KEY, ${DbConstants.valueColumn} TEXT)''', '''CREATE TABLE ${DbConstants.settingsTable} (${DbConstants.keyColumn} TEXT PRIMARY KEY, ${DbConstants.valueColumn} TEXT)''',
); );
await db.execute( await db.execute(
'''CREATE TABLE ${DbConstants.srsItemsTable} (${DbConstants.kanjiIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.readingTypeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.kanjiIdColumn}, ${DbConstants.quizModeColumn}, ${DbConstants.readingTypeColumn}))''', '''CREATE TABLE ${DbConstants.srsItemsTable} (${DbConstants.kanjiIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.readingTypeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, ${DbConstants.disabledColumn} INTEGER DEFAULT 0, PRIMARY KEY (${DbConstants.kanjiIdColumn}, ${DbConstants.quizModeColumn}, ${DbConstants.readingTypeColumn}))''',
); );
await db.execute( await db.execute(
'''CREATE TABLE ${DbConstants.vocabularyTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.readingsColumn} TEXT, ${DbConstants.pronunciationAudiosColumn} TEXT)''', '''CREATE TABLE ${DbConstants.vocabularyTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.readingsColumn} TEXT, ${DbConstants.pronunciationAudiosColumn} TEXT)''',
); );
await db.execute( await db.execute(
'''CREATE TABLE ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''', '''CREATE TABLE ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, ${DbConstants.disabledColumn} INTEGER DEFAULT 0, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''',
); );
}, },
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 8) {
await db.execute('ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0');
await db.execute('ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0');
}
},
); );
} }
} }

View File

@@ -103,6 +103,7 @@ class DeckRepository {
readingType: r[DbConstants.readingTypeColumn] as String?, readingType: r[DbConstants.readingTypeColumn] as String?,
srsStage: r[DbConstants.srsStageColumn] as int, srsStage: r[DbConstants.srsStageColumn] as int,
lastAsked: DateTime.parse(r[DbConstants.lastAskedColumn] as String), lastAsked: DateTime.parse(r[DbConstants.lastAskedColumn] as String),
disabled: (r[DbConstants.disabledColumn] as int? ?? 0) == 1,
); );
srsItemsByKanjiId.putIfAbsent(srsItem.subjectId, () => []).add(srsItem); srsItemsByKanjiId.putIfAbsent(srsItem.subjectId, () => []).add(srsItem);
} }
@@ -118,17 +119,53 @@ class DeckRepository {
return kanjiItems; return kanjiItems;
} }
Future<void> updateSrsItems(List<SrsItem> items) async {
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final item in items) {
var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
final whereArgs = [item.subjectId, item.quizMode.toString()];
if (item.readingType != null) {
where += ' AND ${DbConstants.readingTypeColumn} = ?';
whereArgs.add(item.readingType!);
} else {
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
}
batch.update(
DbConstants.srsItemsTable,
{
DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
},
where: where,
whereArgs: whereArgs,
);
}
await batch.commit(noResult: true);
}
Future<void> updateSrsItem(SrsItem item) async { Future<void> updateSrsItem(SrsItem item) async {
final db = await DatabaseHelper().db; final db = await DatabaseHelper().db;
var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
final whereArgs = [item.subjectId, item.quizMode.toString()];
if (item.readingType != null) {
where += ' AND ${DbConstants.readingTypeColumn} = ?';
whereArgs.add(item.readingType!);
} else {
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
}
await db.update( await db.update(
DbConstants.srsItemsTable, DbConstants.srsItemsTable,
{ {
DbConstants.srsStageColumn: item.srsStage, DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(), DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
}, },
where: where: where,
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ? AND ${DbConstants.readingTypeColumn} = ?', whereArgs: whereArgs,
whereArgs: [item.subjectId, item.quizMode.toString(), item.readingType],
); );
} }
@@ -140,6 +177,7 @@ class DeckRepository {
DbConstants.readingTypeColumn: item.readingType, DbConstants.readingTypeColumn: item.readingType,
DbConstants.srsStageColumn: item.srsStage, DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(), DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
DbConstants.disabledColumn: item.disabled ? 1 : 0,
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }

View File

@@ -68,10 +68,29 @@ class VocabDeckRepository {
), ),
srsStage: r['srsStage'] as int, srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String), lastAsked: DateTime.parse(r['lastAsked'] as String),
disabled: (r['disabled'] as int? ?? 0) == 1,
); );
}).toList(); }).toList();
} }
Future<void> updateSrsItems(List<SrsItem> items) async {
final db = await DatabaseHelper().db;
final batch = db.batch();
for (final item in items) {
batch.update(
'srs_vocab_items',
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
},
where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.subjectId, item.quizMode.toString()],
);
}
await batch.commit(noResult: true);
}
Future<void> updateVocabSrsItem(SrsItem item) async { Future<void> updateVocabSrsItem(SrsItem item) async {
final db = await DatabaseHelper().db; final db = await DatabaseHelper().db;
await db.update( await db.update(
@@ -79,6 +98,7 @@ class VocabDeckRepository {
{ {
'srsStage': item.srsStage, 'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(), 'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
}, },
where: 'vocabId = ? AND quizMode = ?', where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.subjectId, item.quizMode.toString()], whereArgs: [item.subjectId, item.quizMode.toString()],
@@ -92,6 +112,7 @@ class VocabDeckRepository {
'quizMode': item.quizMode.toString(), 'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage, 'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(), 'lastAsked': item.lastAsked.toIso8601String(),
'disabled': item.disabled ? 1 : 0,
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }

View File

@@ -41,15 +41,13 @@ class OptionsGrid extends StatelessWidget {
if (showResult) { if (showResult) {
if (correctAnswers != null && correctAnswers!.contains(o)) { if (correctAnswers != null && correctAnswers!.contains(o)) {
currentButtonColor = theme.colorScheme.tertiary; currentButtonColor = theme.colorScheme.tertiary;
} else if (o == selectedOption) {
currentButtonColor = theme.colorScheme.error;
} }
} }
return SizedBox( return SizedBox(
width: 160, width: 160,
child: ElevatedButton( child: ElevatedButton(
onPressed: isDisabled ? null : () => onSelected(o), onPressed: isDisabled || o == '---' ? null : () => onSelected(o),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: currentButtonColor, backgroundColor: currentButtonColor,
foregroundColor: currentTextColor, foregroundColor: currentTextColor,