359 lines
11 KiB
Dart
359 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:math';
|
|
import '../models/custom_kanji_item.dart';
|
|
import '../widgets/options_grid.dart';
|
|
import '../widgets/kanji_card.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../services/tts_service.dart';
|
|
|
|
enum CustomQuizMode {
|
|
japaneseToEnglish,
|
|
englishToJapanese,
|
|
listeningComprehension,
|
|
}
|
|
|
|
class CustomQuizScreen extends StatefulWidget {
|
|
final List<CustomKanjiItem> deck;
|
|
final CustomQuizMode quizMode;
|
|
final Function(CustomKanjiItem) onCardReviewed;
|
|
final bool useKanji;
|
|
final bool isActive;
|
|
|
|
const CustomQuizScreen({
|
|
super.key,
|
|
required this.deck,
|
|
required this.quizMode,
|
|
required this.onCardReviewed,
|
|
required this.useKanji,
|
|
required this.isActive,
|
|
});
|
|
|
|
@override
|
|
State<CustomQuizScreen> createState() => CustomQuizScreenState();
|
|
}
|
|
|
|
class CustomQuizScreenState extends State<CustomQuizScreen>
|
|
with TickerProviderStateMixin {
|
|
int _currentIndex = 0;
|
|
List<CustomKanjiItem> _shuffledDeck = [];
|
|
List<String> _options = [];
|
|
bool _answered = false;
|
|
bool? _correct;
|
|
late AnimationController _shakeController;
|
|
late Animation<double> _shakeAnimation;
|
|
final List<String> _incorrectlyAnsweredItems = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_shuffledDeck = widget.deck.toList()..shuffle();
|
|
if (_shuffledDeck.isNotEmpty) {
|
|
_generateOptions();
|
|
}
|
|
|
|
_shakeController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
|
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
}
|
|
|
|
@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();
|
|
});
|
|
}
|
|
}
|
|
|
|
void playAudio() async {
|
|
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
|
_currentIndex < _shuffledDeck.length) {
|
|
final ttsService = Provider.of<TtsService>(context, listen: false);
|
|
await ttsService.speak(_shuffledDeck[_currentIndex].characters);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
void dispose() {
|
|
_shakeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _generateOptions() {
|
|
final currentItem = _shuffledDeck[_currentIndex];
|
|
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
|
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
|
_options = [currentItem.meaning];
|
|
}
|
|
else {
|
|
_options = [
|
|
widget.useKanji && currentItem.kanji != null
|
|
? currentItem.kanji!
|
|
: currentItem.characters,
|
|
];
|
|
}
|
|
final otherItems = widget.deck
|
|
.where((item) => item.characters != currentItem.characters)
|
|
.toList();
|
|
otherItems.shuffle();
|
|
for (var i = 0; i < min(3, otherItems.length); i++) {
|
|
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
|
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
|
_options.add(otherItems[i].meaning);
|
|
} else {
|
|
_options.add(
|
|
widget.useKanji && otherItems[i].kanji != null
|
|
? otherItems[i].kanji!
|
|
: otherItems[i].characters,
|
|
);
|
|
}
|
|
}
|
|
_options.shuffle();
|
|
}
|
|
|
|
void _checkAnswer(String answer) async {
|
|
final currentItem = _shuffledDeck[_currentIndex];
|
|
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
|
? (widget.useKanji && currentItem.kanji != null
|
|
? currentItem.kanji!
|
|
: currentItem.characters)
|
|
: currentItem.meaning;
|
|
final isCorrect = answer == correctAnswer;
|
|
|
|
if (currentItem.useInterval) {
|
|
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);
|
|
}
|
|
|
|
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 ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
duration: const Duration(milliseconds: 900),
|
|
);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(snack);
|
|
}
|
|
|
|
if (isCorrect) {
|
|
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
|
widget.quizMode == CustomQuizMode.englishToJapanese) {
|
|
await _speak(currentItem.characters);
|
|
}
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
} else {
|
|
_shakeController.forward(from: 0);
|
|
await Future.delayed(const Duration(milliseconds: 900));
|
|
}
|
|
|
|
_nextQuestion();
|
|
}
|
|
|
|
Future<void> _nextQuestion() async {
|
|
setState(() {
|
|
_currentIndex++;
|
|
_answered = false;
|
|
_correct = null;
|
|
if (_currentIndex < _shuffledDeck.length) {
|
|
_generateOptions();
|
|
}
|
|
});
|
|
if (_currentIndex < _shuffledDeck.length &&
|
|
widget.quizMode == CustomQuizMode.listeningComprehension) {
|
|
await _speak(_shuffledDeck[_currentIndex].characters);
|
|
}
|
|
}
|
|
|
|
Future<void> _speak(String text) async {
|
|
final ttsService = Provider.of<TtsService>(context, listen: false);
|
|
await ttsService.speak(text);
|
|
}
|
|
|
|
void _onOptionSelected(String option) {
|
|
if (!(_answered && _correct!)) {
|
|
_checkAnswer(option);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) {
|
|
return Center(child: Text('Review session complete!', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)));
|
|
}
|
|
|
|
final currentItem = _shuffledDeck[_currentIndex];
|
|
final question = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
|
? currentItem.meaning
|
|
: (widget.useKanji && currentItem.kanji != null
|
|
? currentItem.kanji!
|
|
: currentItem.characters);
|
|
|
|
Widget promptWidget;
|
|
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
|
promptWidget = IconButton(
|
|
icon: const Icon(Icons.volume_up, size: 64),
|
|
onPressed: () => _speak(currentItem.characters),
|
|
);
|
|
} else if (widget.quizMode == CustomQuizMode.englishToJapanese) {
|
|
promptWidget = Text(
|
|
question,
|
|
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
|
textAlign: TextAlign.center,
|
|
);
|
|
} else {
|
|
promptWidget = GestureDetector(
|
|
onTap: () => _speak(question),
|
|
child: Text(
|
|
question,
|
|
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
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: 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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|