add a shit ton of feature for the custom srs

This commit is contained in:
Rene Kievits
2025-10-30 03:44:04 +01:00
parent b58a4020e1
commit ee4fd7ffc1
13 changed files with 787 additions and 311 deletions

View File

@@ -9,16 +9,23 @@ enum CustomQuizMode { japaneseToEnglish, englishToJapanese, listeningComprehensi
class CustomQuizScreen extends StatefulWidget {
final List<CustomKanjiItem> deck;
final CustomQuizMode quizMode;
final Function(CustomKanjiItem) onCardReviewed;
final bool useKanji;
const CustomQuizScreen(
{super.key, required this.deck, required this.quizMode});
const CustomQuizScreen({
super.key,
required this.deck,
required this.quizMode,
required this.onCardReviewed,
required this.useKanji,
});
@override
State<CustomQuizScreen> createState() => CustomQuizScreenState();
}
class CustomQuizScreenState extends State<CustomQuizScreen>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
with TickerProviderStateMixin {
int _currentIndex = 0;
List<CustomKanjiItem> _shuffledDeck = [];
List<String> _options = [];
@@ -27,17 +34,15 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
late FlutterTts _flutterTts;
late AnimationController _shakeController;
late Animation<double> _shakeAnimation;
bool _useKanji = false;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_shuffledDeck = widget.deck.toList()..shuffle();
_initTts();
_generateOptions();
if (_shuffledDeck.isNotEmpty) {
_generateOptions();
}
_shakeController = AnimationController(
duration: const Duration(milliseconds: 500),
@@ -51,6 +56,16 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
);
}
@override
void didUpdateWidget(CustomQuizScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.useKanji != oldWidget.useKanji) {
setState(() {
_generateOptions();
});
}
}
void playAudio() {
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(_shuffledDeck[_currentIndex].characters);
@@ -60,7 +75,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
void _initTts() async {
_flutterTts = FlutterTts();
await _flutterTts.setLanguage("ja-JP");
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
if (_shuffledDeck.isNotEmpty && widget.quizMode == CustomQuizMode.listeningComprehension) {
_speak(_shuffledDeck[_currentIndex].characters);
}
}
@@ -77,7 +92,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
_options = [currentItem.meaning];
} else {
_options = [_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters];
_options = [widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters];
}
final otherItems = widget.deck
.where((item) => item.characters != currentItem.characters)
@@ -87,7 +102,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
_options.add(otherItems[i].meaning);
} else {
_options.add(_useKanji && otherItems[i].kanji != null ? otherItems[i].kanji! : otherItems[i].characters);
_options.add(widget.useKanji && otherItems[i].kanji != null ? otherItems[i].kanji! : otherItems[i].characters);
}
}
_options.shuffle();
@@ -96,10 +111,22 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
void _checkAnswer(String answer) {
final currentItem = _shuffledDeck[_currentIndex];
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
? (_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
: currentItem.meaning;
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));
}
widget.onCardReviewed(currentItem);
}
setState(() {
_answered = true;
_correct = isCorrect;
@@ -139,91 +166,65 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
@override
Widget build(BuildContext context) {
super.build(context);
if (_shuffledDeck.isEmpty) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('No cards in the deck!'),
),
return const Center(
child: Text('Review session complete!'),
);
}
final currentItem = _shuffledDeck[_currentIndex];
final question = (widget.quizMode == CustomQuizMode.englishToJapanese)
? currentItem.meaning
: (_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
: (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
return Scaffold(
appBar: AppBar(
title: const Text('Quiz'),
actions: [
if (widget.quizMode != CustomQuizMode.listeningComprehension)
Row(
children: [
const Text('Kanji'),
Switch(
value: _useKanji,
onChanged: (value) {
setState(() {
_useKanji = value;
_generateOptions();
});
},
),
],
return Center(
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: 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'),
),
],
),
body: Center(
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: 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'),
),
],
),
),
);
}
}