change a bunch of stuff, seperate tracking for progress, updated custom srs layout
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user