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 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 createState() => CustomQuizScreenState(); } class CustomQuizScreenState extends State with TickerProviderStateMixin { int _currentIndex = 0; List _shuffledDeck = []; List _options = []; bool _answered = false; bool? _correct; late AnimationController _shakeController; late Animation _shakeAnimation; final List _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(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(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 _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 _speak(String text) async { final ttsService = Provider.of(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, ), ), ], ), ), ], ), ); } }