change a bunch of stuff, seperate tracking for progress, updated custom srs layout

This commit is contained in:
Rene Kievits
2025-10-31 07:16:44 +01:00
parent cafec12888
commit d8edfa1686
12 changed files with 1378 additions and 661 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flutter_tts/flutter_tts.dart';
import '../models/custom_kanji_item.dart';
import '../widgets/options_grid.dart';
import '../widgets/kanji_card.dart';
enum CustomQuizMode { japaneseToEnglish, englishToJapanese, listeningComprehension }
@@ -11,6 +12,7 @@ class CustomQuizScreen extends StatefulWidget {
final CustomQuizMode quizMode;
final Function(CustomKanjiItem) onCardReviewed;
final bool useKanji;
final bool isActive;
const CustomQuizScreen({
super.key,
@@ -18,6 +20,7 @@ class CustomQuizScreen extends StatefulWidget {
required this.quizMode,
required this.onCardReviewed,
required this.useKanji,
required this.isActive,
});
@override
@@ -34,6 +37,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
late FlutterTts _flutterTts;
late AnimationController _shakeController;
late Animation<double> _shakeAnimation;
final List<String> _incorrectlyAnsweredItems = [];
@override
void initState() {
@@ -59,6 +63,17 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
@override
void didUpdateWidget(CustomQuizScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.deck != oldWidget.deck && !widget.isActive) {
setState(() {
_shuffledDeck = widget.deck.toList()..shuffle();
_currentIndex = 0;
_answered = false;
_correct = null;
if (_shuffledDeck.isNotEmpty) {
_generateOptions();
}
});
}
if (widget.useKanji != oldWidget.useKanji) {
setState(() {
_generateOptions();
@@ -67,7 +82,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
}
void playAudio() {
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
if (widget.quizMode == CustomQuizMode.listeningComprehension && _currentIndex < _shuffledDeck.length) {
_speak(_shuffledDeck[_currentIndex].characters);
}
}
@@ -75,9 +90,6 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
void _initTts() async {
_flutterTts = FlutterTts();
await _flutterTts.setLanguage("ja-JP");
if (_shuffledDeck.isNotEmpty && widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(_shuffledDeck[_currentIndex].characters);
}
}
@override
@@ -108,7 +120,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
_options.shuffle();
}
void _checkAnswer(String answer) {
void _checkAnswer(String answer) async {
final currentItem = _shuffledDeck[_currentIndex];
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
@@ -116,42 +128,118 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
final isCorrect = answer == correctAnswer;
if (currentItem.useInterval) {
if (isCorrect) {
currentItem.srsLevel++;
final interval = pow(2, currentItem.srsLevel).toInt();
currentItem.nextReview = DateTime.now().add(Duration(hours: interval));
} else {
currentItem.srsLevel = max(0, currentItem.srsLevel - 1);
currentItem.nextReview = DateTime.now().add(const Duration(hours: 1));
int currentSrsLevel;
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
currentSrsLevel = currentItem.srsData.japaneseToEnglish;
break;
case CustomQuizMode.englishToJapanese:
currentSrsLevel = currentItem.srsData.englishToJapanese;
break;
case CustomQuizMode.listeningComprehension:
currentSrsLevel = currentItem.srsData.listeningComprehension;
break;
}
if (isCorrect) {
if (_incorrectlyAnsweredItems.contains(currentItem.characters)) {
_incorrectlyAnsweredItems.remove(currentItem.characters);
} else {
currentSrsLevel++;
}
final interval = pow(2, currentSrsLevel).toInt();
final newNextReview = DateTime.now().add(Duration(hours: interval));
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
currentItem.srsData.japaneseToEnglishNextReview = newNextReview;
break;
case CustomQuizMode.englishToJapanese:
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
break;
case CustomQuizMode.listeningComprehension:
currentItem.srsData.listeningComprehensionNextReview = newNextReview;
break;
}
} else {
if (!_incorrectlyAnsweredItems.contains(currentItem.characters)) {
_incorrectlyAnsweredItems.add(currentItem.characters);
}
currentSrsLevel = max(0, currentSrsLevel - 1);
final newNextReview = DateTime.now().add(const Duration(hours: 1));
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
currentItem.srsData.japaneseToEnglishNextReview = newNextReview;
break;
case CustomQuizMode.englishToJapanese:
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
break;
case CustomQuizMode.listeningComprehension:
currentItem.srsData.listeningComprehensionNextReview = newNextReview;
break;
}
}
switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish:
currentItem.srsData.japaneseToEnglish = currentSrsLevel;
break;
case CustomQuizMode.englishToJapanese:
currentItem.srsData.englishToJapanese = currentSrsLevel;
break;
case CustomQuizMode.listeningComprehension:
currentItem.srsData.listeningComprehension = currentSrsLevel;
break;
}
widget.onCardReviewed(currentItem);
}
setState(() {
_answered = true;
_correct = isCorrect;
});
// --- SnackBar Logic (new) ---
final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese)
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
: currentItem.meaning;
final snack = SnackBar(
content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle(
color: isCorrect ? Colors.greenAccent : Colors.redAccent,
fontWeight: FontWeight.bold,
),
),
backgroundColor: const Color(0xFF222222),
duration: const Duration(milliseconds: 900),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snack);
}
// --- End SnackBar Logic ---
if (isCorrect) {
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(currentItem.characters);
if (widget.quizMode == CustomQuizMode.japaneseToEnglish) {
await _speak(currentItem.characters);
}
await Future.delayed(const Duration(milliseconds: 500)); // Small delay after correct answer
} else {
_shakeController.forward(from: 0);
await Future.delayed(const Duration(milliseconds: 900)); // Delay for shake animation
}
_nextQuestion();
}
void _nextQuestion() {
setState(() {
_currentIndex = (_currentIndex + 1) % _shuffledDeck.length;
_currentIndex++;
_answered = false;
_correct = null;
_generateOptions();
if (_currentIndex < _shuffledDeck.length) {
_generateOptions();
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(_shuffledDeck[_currentIndex].characters);
}
}
});
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(_shuffledDeck[_currentIndex].characters);
}
}
Future<void> _speak(String text) async {
@@ -166,7 +254,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
@override
Widget build(BuildContext context) {
if (_shuffledDeck.isEmpty) {
if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) {
return const Center(
child: Text('Review session complete!'),
);
@@ -177,54 +265,70 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
? currentItem.meaning
: (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
return Center(
Widget promptWidget;
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
promptWidget = IconButton(
icon: const Icon(Icons.volume_up, size: 64),
onPressed: () => _speak(currentItem.characters),
);
} else {
promptWidget = GestureDetector(
onTap: () => _speak(question),
child: Text(
question,
style: const TextStyle(fontSize: 48),
textAlign: TextAlign.center,
),
);
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.quizMode == CustomQuizMode.listeningComprehension)
IconButton(
icon: const Icon(Icons.volume_up, size: 64),
onPressed: () => _speak(currentItem.characters),
)
else
GestureDetector(
onTap: () => _speak(question),
child: Text(
question,
style: const TextStyle(fontSize: 48),
textAlign: TextAlign.center,
const SizedBox(height: 18),
Expanded(
flex: 3,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 0,
maxWidth: 500,
minHeight: 150,
),
child: KanjiCard(
characterWidget: promptWidget,
subtitle: '',
),
),
),
const SizedBox(height: 32),
if (_answered)
Text(
_correct! ? 'Correct!' : 'Incorrect, try again!',
style: TextStyle(
fontSize: 24,
color: _correct! ? Colors.green : Colors.red,
),
),
const SizedBox(height: 32),
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_shakeAnimation.value * 10, 0),
child: child,
);
},
child: OptionsGrid(
options: _options,
onSelected: _onOptionSelected,
),
),
if (_answered && _correct!)
ElevatedButton(
onPressed: _nextQuestion,
child: const Text('Next'),
const SizedBox(height: 12),
SafeArea(
top: false,
child: Column(
children: [
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_shakeAnimation.value * 10, 0),
child: child,
);
},
child: OptionsGrid(
options: _options,
onSelected: _onOptionSelected,
correctAnswers: [],
showResult: false,
isDisabled: false,
),
),
],
),
),
],
),
);
}
}
}