finish v3
This commit is contained in:
@@ -88,6 +88,7 @@ class WkClient {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static Subject createSubjectFromMap(Map<String, dynamic> map) {
|
||||
final String object = map['object'];
|
||||
if (object == 'kanji') {
|
||||
|
||||
@@ -254,7 +254,7 @@ class _BrowseScreenState extends State<BrowseScreen>
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: isDisabled
|
||||
? Theme.of(context).colorScheme.surfaceVariant
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
|
||||
foregroundColor: isSelected
|
||||
|
||||
@@ -34,34 +34,37 @@ class CustomQuizScreen extends StatefulWidget {
|
||||
State<CustomQuizScreen> createState() => CustomQuizScreenState();
|
||||
}
|
||||
|
||||
class _CustomQuizState {
|
||||
CustomKanjiItem? current;
|
||||
List<String> options = [];
|
||||
List<String> correctAnswers = [];
|
||||
int score = 0;
|
||||
int asked = 0;
|
||||
Key key = UniqueKey();
|
||||
String? selectedOption;
|
||||
bool showResult = false;
|
||||
Set<String> wrongItems = {};
|
||||
}
|
||||
|
||||
class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
final _quizState = _CustomQuizState();
|
||||
List<CustomKanjiItem> _shuffledDeck = [];
|
||||
List<String> _options = [];
|
||||
bool _answered = false;
|
||||
bool? _correct;
|
||||
int _sessionDeckSize = 0;
|
||||
bool _isAnswering = false;
|
||||
late AnimationController _shakeController;
|
||||
late Animation<double> _shakeAnimation;
|
||||
final List<String> _incorrectlyAnsweredItems = [];
|
||||
final _audioPlayer = AudioPlayer();
|
||||
|
||||
bool _playIncorrectSound = true;
|
||||
bool _playCorrectSound = true;
|
||||
bool _playNarrator = true;
|
||||
|
||||
String? _selectedOption;
|
||||
String? _correctAnswer;
|
||||
bool _showResult = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shuffledDeck = widget.deck.toList()..shuffle();
|
||||
if (_shuffledDeck.isNotEmpty) {
|
||||
_generateOptions();
|
||||
}
|
||||
|
||||
_sessionDeckSize = _shuffledDeck.length;
|
||||
_shakeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
@@ -70,6 +73,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
|
||||
);
|
||||
_loadSettings();
|
||||
_nextQuestion();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
@@ -90,163 +94,53 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
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();
|
||||
}
|
||||
});
|
||||
_shuffledDeck = widget.deck.toList()..shuffle();
|
||||
_sessionDeckSize = _shuffledDeck.length;
|
||||
_nextQuestion();
|
||||
}
|
||||
if (widget.useKanji != oldWidget.useKanji) {
|
||||
setState(() {
|
||||
_generateOptions();
|
||||
});
|
||||
_nextQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
void playAudio() async {
|
||||
final quizState = _quizState;
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
||||
_currentIndex < _shuffledDeck.length && _playNarrator) {
|
||||
quizState.current != null &&
|
||||
_playNarrator) {
|
||||
final ttsService = Provider.of<TtsService>(context, listen: false);
|
||||
await ttsService.speak(_shuffledDeck[_currentIndex].characters);
|
||||
await ttsService.speak(quizState.current!.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
while (_options.length < 4) {
|
||||
_options.add('---');
|
||||
}
|
||||
_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;
|
||||
void _answer(String option) async {
|
||||
final quizState = _quizState;
|
||||
final current = quizState.current!;
|
||||
final isCorrect = quizState.correctAnswers
|
||||
.map((a) => a.toLowerCase().trim())
|
||||
.contains(option.toLowerCase().trim());
|
||||
|
||||
setState(() {
|
||||
_selectedOption = answer;
|
||||
_correctAnswer = correctAnswer;
|
||||
_showResult = true;
|
||||
quizState.selectedOption = option;
|
||||
quizState.showResult = true;
|
||||
_isAnswering = true;
|
||||
});
|
||||
|
||||
int currentSrsLevel = 0; // Initialize with a default value
|
||||
if (currentItem.useInterval) {
|
||||
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);
|
||||
if (current.useInterval) {
|
||||
_updateSrsLevel(current, isCorrect);
|
||||
}
|
||||
|
||||
final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||
? (widget.useKanji && currentItem.kanji != null
|
||||
? currentItem.kanji!
|
||||
: currentItem.characters)
|
||||
: currentItem.meaning;
|
||||
? (widget.useKanji && current.kanji != null
|
||||
? current.kanji!
|
||||
: current.characters)
|
||||
: current.meaning;
|
||||
|
||||
final snack = SnackBar(
|
||||
content: Text(
|
||||
@@ -266,52 +160,23 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
}
|
||||
|
||||
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;
|
||||
quizState.asked += 1;
|
||||
if (!quizState.wrongItems.contains(current.characters)) {
|
||||
quizState.score += 1;
|
||||
}
|
||||
if (_playCorrectSound && !_playNarrator) {
|
||||
await _audioPlayer.play(AssetSource('sfx/correct.wav'));
|
||||
} else if (_playNarrator) {
|
||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
||||
widget.quizMode == CustomQuizMode.englishToJapanese) {
|
||||
await _speak(currentItem.characters);
|
||||
await _speak(current.characters);
|
||||
}
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
} 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;
|
||||
}
|
||||
quizState.wrongItems.add(current.characters);
|
||||
_shuffledDeck.add(current);
|
||||
_shuffledDeck.shuffle();
|
||||
if (_playIncorrectSound) {
|
||||
await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
|
||||
}
|
||||
@@ -319,24 +184,132 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
await Future.delayed(const Duration(milliseconds: 900));
|
||||
}
|
||||
|
||||
_nextQuestion();
|
||||
}
|
||||
|
||||
Future<void> _nextQuestion() async {
|
||||
setState(() {
|
||||
_currentIndex++;
|
||||
_answered = false;
|
||||
_correct = null;
|
||||
_selectedOption = null;
|
||||
_correctAnswer = null;
|
||||
_showResult = false;
|
||||
if (_currentIndex < _shuffledDeck.length) {
|
||||
_generateOptions();
|
||||
Future.delayed(const Duration(milliseconds: 900), () {
|
||||
if (mounted) {
|
||||
_nextQuestion();
|
||||
}
|
||||
});
|
||||
if (_currentIndex < _shuffledDeck.length &&
|
||||
}
|
||||
|
||||
void _updateSrsLevel(CustomKanjiItem item, bool isCorrect) {
|
||||
int currentSrsLevel = 0;
|
||||
switch (widget.quizMode) {
|
||||
case CustomQuizMode.japaneseToEnglish:
|
||||
currentSrsLevel = item.srsData.japaneseToEnglish;
|
||||
break;
|
||||
case CustomQuizMode.englishToJapanese:
|
||||
currentSrsLevel = item.srsData.englishToJapanese;
|
||||
break;
|
||||
case CustomQuizMode.listeningComprehension:
|
||||
currentSrsLevel = item.srsData.listeningComprehension;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
currentSrsLevel++;
|
||||
final interval = pow(2, currentSrsLevel).toInt();
|
||||
final newNextReview = DateTime.now().add(Duration(hours: interval));
|
||||
switch (widget.quizMode) {
|
||||
case CustomQuizMode.japaneseToEnglish:
|
||||
item.srsData.japaneseToEnglishNextReview = newNextReview;
|
||||
break;
|
||||
case CustomQuizMode.englishToJapanese:
|
||||
item.srsData.englishToJapaneseNextReview = newNextReview;
|
||||
break;
|
||||
case CustomQuizMode.listeningComprehension:
|
||||
item.srsData.listeningComprehensionNextReview = newNextReview;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
currentSrsLevel = max(0, currentSrsLevel - 1);
|
||||
final newNextReview = DateTime.now().add(const Duration(hours: 1));
|
||||
switch (widget.quizMode) {
|
||||
case CustomQuizMode.japaneseToEnglish:
|
||||
item.srsData.japaneseToEnglishNextReview = newNextReview;
|
||||
break;
|
||||
case CustomQuizMode.englishToJapanese:
|
||||
item.srsData.englishToJapaneseNextReview = newNextReview;
|
||||
break;
|
||||
case CustomQuizMode.listeningComprehension:
|
||||
item.srsData.listeningComprehensionNextReview = newNextReview;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (widget.quizMode) {
|
||||
case CustomQuizMode.japaneseToEnglish:
|
||||
item.srsData.japaneseToEnglish = currentSrsLevel;
|
||||
break;
|
||||
case CustomQuizMode.englishToJapanese:
|
||||
item.srsData.englishToJapanese = currentSrsLevel;
|
||||
break;
|
||||
case CustomQuizMode.listeningComprehension:
|
||||
item.srsData.listeningComprehension = currentSrsLevel;
|
||||
break;
|
||||
}
|
||||
|
||||
widget.onCardReviewed(item);
|
||||
}
|
||||
|
||||
void _nextQuestion() {
|
||||
final quizState = _quizState;
|
||||
|
||||
if (_shuffledDeck.isEmpty) {
|
||||
setState(() {
|
||||
quizState.current = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
quizState.current = _shuffledDeck.removeAt(0);
|
||||
quizState.key = UniqueKey();
|
||||
|
||||
quizState.correctAnswers = [];
|
||||
quizState.options = [];
|
||||
quizState.selectedOption = null;
|
||||
quizState.showResult = false;
|
||||
|
||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
||||
widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
await _speak(_shuffledDeck[_currentIndex].characters);
|
||||
quizState.correctAnswers = [quizState.current!.meaning];
|
||||
quizState.options = [quizState.correctAnswers.first];
|
||||
} else {
|
||||
quizState.correctAnswers = [
|
||||
widget.useKanji && quizState.current!.kanji != null
|
||||
? quizState.current!.kanji!
|
||||
: quizState.current!.characters,
|
||||
];
|
||||
quizState.options = [quizState.correctAnswers.first];
|
||||
}
|
||||
|
||||
final otherItems = widget.deck
|
||||
.where((item) => item.characters != quizState.current!.characters)
|
||||
.toList();
|
||||
otherItems.shuffle();
|
||||
|
||||
for (var i = 0; i < min(3, otherItems.length); i++) {
|
||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
||||
widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
quizState.options.add(otherItems[i].meaning);
|
||||
} else {
|
||||
quizState.options.add(
|
||||
widget.useKanji && otherItems[i].kanji != null
|
||||
? otherItems[i].kanji!
|
||||
: otherItems[i].characters,
|
||||
);
|
||||
}
|
||||
}
|
||||
while (quizState.options.length < 4) {
|
||||
quizState.options.add('---');
|
||||
}
|
||||
quizState.options.shuffle();
|
||||
|
||||
setState(() {
|
||||
_isAnswering = false;
|
||||
});
|
||||
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
_speak(quizState.current!.characters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,25 +319,29 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
}
|
||||
|
||||
void _onOptionSelected(String option) {
|
||||
if (!(_answered && _correct!)) {
|
||||
_checkAnswer(option);
|
||||
if (!_isAnswering) {
|
||||
_answer(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 quizState = _quizState;
|
||||
|
||||
if (quizState.current == null) {
|
||||
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);
|
||||
final currentItem = quizState.current!;
|
||||
|
||||
Widget promptWidget;
|
||||
String subtitle = '';
|
||||
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
promptWidget = IconButton(
|
||||
icon: const Icon(Icons.volume_up, size: 64),
|
||||
@@ -372,25 +349,60 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
);
|
||||
} else if (widget.quizMode == CustomQuizMode.englishToJapanese) {
|
||||
promptWidget = Text(
|
||||
question,
|
||||
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
||||
currentItem.meaning,
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
} else {
|
||||
final promptText = widget.useKanji && currentItem.kanji != null
|
||||
? currentItem.kanji!
|
||||
: currentItem.characters;
|
||||
promptWidget = GestureDetector(
|
||||
onTap: () => _speak(question),
|
||||
onTap: () => _speak(promptText),
|
||||
child: Text(
|
||||
question,
|
||||
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
||||
promptText,
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
key: quizState.key,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${quizState.asked} / $_sessionDeckSize',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: _sessionDeckSize > 0
|
||||
? quizState.asked / _sessionDeckSize
|
||||
: 0,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
@@ -401,7 +413,10 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
maxWidth: 500,
|
||||
minHeight: 150,
|
||||
),
|
||||
child: KanjiCard(characterWidget: promptWidget, subtitle: ''),
|
||||
child: KanjiCard(
|
||||
characterWidget: promptWidget,
|
||||
subtitle: subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -419,11 +434,18 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
);
|
||||
},
|
||||
child: OptionsGrid(
|
||||
options: _options,
|
||||
onSelected: _onOptionSelected,
|
||||
correctAnswers: [],
|
||||
showResult: false,
|
||||
isDisabled: false,
|
||||
options: quizState.options,
|
||||
onSelected: _isAnswering ? (option) {} : _onOptionSelected,
|
||||
selectedOption: quizState.selectedOption,
|
||||
correctAnswers: quizState.correctAnswers,
|
||||
showResult: quizState.showResult,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Score: ${quizState.score} / ${quizState.asked}',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -128,8 +128,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
|
||||
itemsByLevel.forEach((level, items) {
|
||||
final allSrsItems = items.expand((item) => item.srsItems.values).toList();
|
||||
if (allSrsItems.isNotEmpty && allSrsItems.every((srs) => srs.disabled)) {
|
||||
final allSrsItems = items
|
||||
.expand((item) => item.srsItems.values)
|
||||
.toList();
|
||||
if (allSrsItems.isNotEmpty &&
|
||||
allSrsItems.every((srs) => srs.disabled)) {
|
||||
disabledLevels.add(level);
|
||||
}
|
||||
});
|
||||
@@ -144,9 +147,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (mode == QuizMode.reading) {
|
||||
final onyomiSrs = item.srsItems['${QuizMode.reading}onyomi'];
|
||||
final kunyomiSrs = item.srsItems['${QuizMode.reading}kunyomi'];
|
||||
final hasOnyomi = item.onyomi.isNotEmpty &&
|
||||
final hasOnyomi =
|
||||
item.onyomi.isNotEmpty &&
|
||||
(onyomiSrs == null || !onyomiSrs.disabled);
|
||||
final hasKunyomi = item.kunyomi.isNotEmpty &&
|
||||
final hasKunyomi =
|
||||
item.kunyomi.isNotEmpty &&
|
||||
(kunyomiSrs == null || !kunyomiSrs.disabled);
|
||||
return hasOnyomi || hasKunyomi;
|
||||
}
|
||||
@@ -289,8 +294,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
String readingType = '';
|
||||
if (mode == QuizMode.reading) {
|
||||
readingType =
|
||||
quizState.readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
|
||||
readingType = quizState.readingHint.contains("on'yomi")
|
||||
? 'onyomi'
|
||||
: 'kunyomi';
|
||||
}
|
||||
final srsKey = mode.toString() + readingType;
|
||||
|
||||
@@ -341,8 +347,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final correctDisplay = (mode == QuizMode.kanjiToEnglish)
|
||||
? _toTitleCase(quizState.correctAnswers.first)
|
||||
: (mode == QuizMode.reading
|
||||
? quizState.correctAnswers.join(', ')
|
||||
: quizState.correctAnswers.first);
|
||||
? quizState.correctAnswers.join(', ')
|
||||
: quizState.correctAnswers.first);
|
||||
|
||||
final snack = SnackBar(
|
||||
content: Text(
|
||||
@@ -449,7 +455,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Text(
|
||||
_status,
|
||||
style: TextStyle(
|
||||
fontSize: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||
fontSize: 24,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -495,10 +503,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
value: (_sessionDeckSizes[index] ?? 0) > 0
|
||||
? quizState.asked / (_sessionDeckSizes[index] ?? 1)
|
||||
: 0,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary),
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -17,9 +17,9 @@ class StartScreen extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const SettingsScreen()));
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -48,9 +48,9 @@ class StartScreen extends StatelessWidget {
|
||||
icon: Icons.extension,
|
||||
description: 'Test your knowledge of kanji characters.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const HomeScreen()));
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
@@ -59,9 +59,9 @@ class StartScreen extends StatelessWidget {
|
||||
icon: Icons.school,
|
||||
description: 'Practice vocabulary from your WaniKani deck.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const VocabScreen()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const VocabScreen()));
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
@@ -70,9 +70,9 @@ class StartScreen extends StatelessWidget {
|
||||
icon: Icons.grid_view,
|
||||
description: 'Look through your kanji and vocabulary decks.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const BrowseScreen()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const BrowseScreen()));
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
@@ -92,7 +92,8 @@ class StartScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeCard(BuildContext context, {
|
||||
Widget _buildModeCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required String description,
|
||||
@@ -109,13 +110,17 @@ class StartScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: Theme.of(context).colorScheme.primary),
|
||||
Icon(
|
||||
icon,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -123,8 +123,11 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
}
|
||||
|
||||
itemsByLevel.forEach((level, items) {
|
||||
final allSrsItems = items.expand((item) => item.srsItems.values).toList();
|
||||
if (allSrsItems.isNotEmpty && allSrsItems.every((srs) => srs.disabled)) {
|
||||
final allSrsItems = items
|
||||
.expand((item) => item.srsItems.values)
|
||||
.toList();
|
||||
if (allSrsItems.isNotEmpty &&
|
||||
allSrsItems.every((srs) => srs.disabled)) {
|
||||
disabledLevels.add(level);
|
||||
}
|
||||
});
|
||||
@@ -349,7 +352,7 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
if (mounted) {
|
||||
_nextQuestion();
|
||||
}
|
||||
});;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -429,7 +432,9 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
child: Text(
|
||||
_status,
|
||||
style: TextStyle(
|
||||
fontSize: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||
fontSize: 24,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -491,10 +496,12 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
value: (_sessionDeckSizes[index] ?? 0) > 0
|
||||
? quizState.asked / (_sessionDeckSizes[index] ?? 1)
|
||||
: 0,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary),
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/custom_kanji_item.dart';
|
||||
@@ -29,7 +28,9 @@ class CustomDeckRepository {
|
||||
Future<void> updateCards(List<CustomKanjiItem> itemsToUpdate) async {
|
||||
final deck = await getCustomDeck();
|
||||
for (var item in itemsToUpdate) {
|
||||
final index = deck.indexWhere((element) => element.characters == item.characters);
|
||||
final index = deck.indexWhere(
|
||||
(element) => element.characters == item.characters,
|
||||
);
|
||||
if (index != -1) {
|
||||
deck[index] = item;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
class DbConstants {
|
||||
static const String settingsTable = 'settings';
|
||||
static const String kanjiTable = 'kanji';
|
||||
|
||||
@@ -52,8 +52,12 @@ class DatabaseHelper {
|
||||
},
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
if (oldVersion < 8) {
|
||||
await db.execute('ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0');
|
||||
await db.execute('ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0');
|
||||
await db.execute(
|
||||
'ALTER TABLE ${DbConstants.srsItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0',
|
||||
);
|
||||
await db.execute(
|
||||
'ALTER TABLE ${DbConstants.srsVocabItemsTable} ADD COLUMN ${DbConstants.disabledColumn} INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -123,7 +123,8 @@ class DeckRepository {
|
||||
final db = await DatabaseHelper().db;
|
||||
final batch = db.batch();
|
||||
for (final item in items) {
|
||||
var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
||||
var where =
|
||||
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
||||
final whereArgs = [item.subjectId, item.quizMode.toString()];
|
||||
if (item.readingType != null) {
|
||||
where += ' AND ${DbConstants.readingTypeColumn} = ?';
|
||||
@@ -148,7 +149,8 @@ class DeckRepository {
|
||||
|
||||
Future<void> updateSrsItem(SrsItem item) async {
|
||||
final db = await DatabaseHelper().db;
|
||||
var where = '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
||||
var where =
|
||||
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
||||
final whereArgs = [item.subjectId, item.quizMode.toString()];
|
||||
if (item.readingType != null) {
|
||||
where += ' AND ${DbConstants.readingTypeColumn} = ?';
|
||||
|
||||
@@ -5,14 +5,26 @@ import 'dart:math';
|
||||
class DistractorGenerator {
|
||||
final Random _rnd = Random();
|
||||
|
||||
List<String> generateMeanings(KanjiItem correct, List<KanjiItem> pool, int needed) {
|
||||
List<String> generateMeanings(
|
||||
KanjiItem correct,
|
||||
List<KanjiItem> pool,
|
||||
int needed,
|
||||
) {
|
||||
final correctMeaning = correct.meanings.first;
|
||||
final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
|
||||
final tokens = correctMeaning
|
||||
.split(RegExp(r'\s+'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
final candidates = <String>[];
|
||||
for (final k in pool) {
|
||||
if (k.id == correct.id) continue;
|
||||
for (final m in k.meanings) {
|
||||
final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
|
||||
final mTokens = m
|
||||
.split(RegExp(r'\s+'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
if (mTokens.intersection(tokens).isNotEmpty) {
|
||||
candidates.add(m);
|
||||
}
|
||||
@@ -39,8 +51,15 @@ class DistractorGenerator {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<String> generateKanji(KanjiItem correct, List<KanjiItem> pool, int needed) {
|
||||
final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList();
|
||||
List<String> generateKanji(
|
||||
KanjiItem correct,
|
||||
List<KanjiItem> pool,
|
||||
int needed,
|
||||
) {
|
||||
final others = pool
|
||||
.map((k) => k.characters)
|
||||
.where((c) => c != correct.characters)
|
||||
.toList();
|
||||
others.shuffle(_rnd);
|
||||
final out = <String>[];
|
||||
for (final o in others) {
|
||||
@@ -53,7 +72,11 @@ class DistractorGenerator {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<String> generateReadings(String correct, List<KanjiItem> pool, int needed) {
|
||||
List<String> generateReadings(
|
||||
String correct,
|
||||
List<KanjiItem> pool,
|
||||
int needed,
|
||||
) {
|
||||
final poolReadings = <String>[];
|
||||
for (final k in pool) {
|
||||
poolReadings.addAll(k.onyomi);
|
||||
@@ -72,14 +95,26 @@ class DistractorGenerator {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<String> generateVocabMeanings(VocabularyItem correct, List<VocabularyItem> pool, int needed) {
|
||||
List<String> generateVocabMeanings(
|
||||
VocabularyItem correct,
|
||||
List<VocabularyItem> pool,
|
||||
int needed,
|
||||
) {
|
||||
final correctMeaning = correct.meanings.first;
|
||||
final tokens = correctMeaning.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
|
||||
final tokens = correctMeaning
|
||||
.split(RegExp(r'\s+'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
final candidates = <String>[];
|
||||
for (final k in pool) {
|
||||
if (k.id == correct.id) continue;
|
||||
for (final m in k.meanings) {
|
||||
final mTokens = m.split(RegExp(r'\s+')).map((s) => s.trim()).where((s) => s.isNotEmpty).toSet();
|
||||
final mTokens = m
|
||||
.split(RegExp(r'\s+'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
if (mTokens.intersection(tokens).isNotEmpty) {
|
||||
candidates.add(m);
|
||||
}
|
||||
@@ -106,8 +141,15 @@ class DistractorGenerator {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<String> generateVocab(VocabularyItem correct, List<VocabularyItem> pool, int needed) {
|
||||
final others = pool.map((k) => k.characters).where((c) => c != correct.characters).toList();
|
||||
List<String> generateVocab(
|
||||
VocabularyItem correct,
|
||||
List<VocabularyItem> pool,
|
||||
int needed,
|
||||
) {
|
||||
final others = pool
|
||||
.map((k) => k.characters)
|
||||
.where((c) => c != correct.characters)
|
||||
.toList();
|
||||
others.shuffle(_rnd);
|
||||
final out = <String>[];
|
||||
for (final o in others) {
|
||||
@@ -121,4 +163,7 @@ class DistractorGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
String _toTitleCase(String s) => s.split(' ').map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1))).join(' ');
|
||||
String _toTitleCase(String s) => s
|
||||
.split(' ')
|
||||
.map((w) => w.isEmpty ? w : (w[0].toUpperCase() + w.substring(1)))
|
||||
.join(' ');
|
||||
|
||||
@@ -26,7 +26,6 @@ class VocabDeckRepository {
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
|
||||
Future<String?> loadApiKey() async {
|
||||
String? envApiKey;
|
||||
try {
|
||||
|
||||
@@ -38,7 +38,8 @@ extension CustomTheme on ThemeData {
|
||||
level8: Color(0xFF4FC3F7), // light blue
|
||||
level9: Color(0xFF7986CB), // indigo
|
||||
);
|
||||
} else if (colorScheme.primary == const Color(0xFF7B6D53)) { // Nier theme
|
||||
} else if (colorScheme.primary == const Color(0xFF7B6D53)) {
|
||||
// Nier theme
|
||||
return const SrsColors(
|
||||
level1: Color(0xFFB71C1C), // dark red
|
||||
level2: Color(0xFFD84315), // deep orange
|
||||
@@ -50,7 +51,8 @@ extension CustomTheme on ThemeData {
|
||||
level8: Color(0xFF0277BD), // light blue
|
||||
level9: Color(0xFF283593), // indigo
|
||||
);
|
||||
} else { // Light theme
|
||||
} else {
|
||||
// Light theme
|
||||
return const SrsColors(
|
||||
level1: Colors.red,
|
||||
level2: Colors.orange,
|
||||
|
||||
@@ -19,13 +19,19 @@ class KanjiCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final bgColor = backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface;
|
||||
final fgColor = textColor ?? theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurface;
|
||||
final bgColor =
|
||||
backgroundColor ?? theme.cardTheme.color ?? theme.colorScheme.surface;
|
||||
final fgColor =
|
||||
textColor ??
|
||||
theme.textTheme.bodyMedium?.color ??
|
||||
theme.colorScheme.onSurface;
|
||||
|
||||
return Card(
|
||||
elevation: theme.cardTheme.elevation ?? 12,
|
||||
color: bgColor,
|
||||
shape: theme.cardTheme.shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape:
|
||||
theme.cardTheme.shape ??
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: SizedBox(
|
||||
width: 360,
|
||||
height: 240,
|
||||
|
||||
@@ -39,7 +39,11 @@ class OptionsGrid extends StatelessWidget {
|
||||
Color currentTextColor = fg;
|
||||
|
||||
if (showResult) {
|
||||
if (correctAnswers != null && correctAnswers!.contains(o)) {
|
||||
final normalizedOption = o.trim().toLowerCase();
|
||||
if (correctAnswers != null &&
|
||||
correctAnswers!
|
||||
.map((e) => e.trim().toLowerCase())
|
||||
.contains(normalizedOption)) {
|
||||
currentButtonColor = theme.colorScheme.tertiary;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +55,8 @@ class OptionsGrid extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: currentButtonColor,
|
||||
foregroundColor: currentTextColor,
|
||||
disabledBackgroundColor:
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -58,7 +64,9 @@ class OptionsGrid extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
o,
|
||||
style: theme.textTheme.titleMedium?.copyWith(color: currentTextColor),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: currentTextColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user