possible to exclude levels and change how questions are served
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user