change a bunch of stuff, seperate tracking for progress, updated custom srs layout
This commit is contained in:
@@ -18,6 +18,18 @@ class _ReadingInfo {
|
||||
_ReadingInfo(this.correctReadings, this.hint);
|
||||
}
|
||||
|
||||
class _QuizState {
|
||||
KanjiItem? current;
|
||||
List<String> options = [];
|
||||
List<String> correctAnswers = [];
|
||||
String readingHint = '';
|
||||
int score = 0;
|
||||
int asked = 0;
|
||||
Key key = UniqueKey();
|
||||
String? selectedOption;
|
||||
bool showResult = false;
|
||||
}
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key, this.distractorGenerator});
|
||||
|
||||
@@ -31,17 +43,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
late TabController _tabController;
|
||||
List<KanjiItem> _deck = [];
|
||||
bool _loading = false;
|
||||
bool _isAnswering = false;
|
||||
String _status = 'Loading deck...';
|
||||
late final DistractorGenerator _dg;
|
||||
final Random _random = Random();
|
||||
final _audioPlayer = AudioPlayer();
|
||||
|
||||
KanjiItem? _current;
|
||||
List<String> _options = [];
|
||||
List<String> _correctAnswers = [];
|
||||
String _readingHint = '';
|
||||
int _score = 0;
|
||||
int _asked = 0;
|
||||
final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
|
||||
_QuizState get _currentQuizState => _quizStates[_tabController.index];
|
||||
|
||||
bool _playCorrectSound = true;
|
||||
bool _apiKeyMissing = false;
|
||||
|
||||
@@ -50,8 +60,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.indexIsChanging) {
|
||||
_nextQuestion();
|
||||
}
|
||||
setState(() {});
|
||||
_nextQuestion();
|
||||
});
|
||||
_dg = widget.distractorGenerator ?? DistractorGenerator();
|
||||
_loadSettings();
|
||||
@@ -105,7 +117,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
_apiKeyMissing = false;
|
||||
});
|
||||
|
||||
_nextQuestion();
|
||||
for (var i = 0; i < _tabController.length; i++) {
|
||||
_nextQuestion(i);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_status = 'Error: $e';
|
||||
@@ -135,8 +149,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
return _ReadingInfo(readingsList, hint);
|
||||
}
|
||||
|
||||
QuizMode get _mode {
|
||||
switch (_tabController.index) {
|
||||
QuizMode _modeForIndex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return QuizMode.kanjiToEnglish;
|
||||
case 1:
|
||||
@@ -148,127 +162,153 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
}
|
||||
}
|
||||
|
||||
void _nextQuestion() {
|
||||
void _nextQuestion([int? index]) {
|
||||
if (_deck.isEmpty) return;
|
||||
|
||||
final quizState = _quizStates[index ?? _tabController.index];
|
||||
final mode = _modeForIndex(index ?? _tabController.index);
|
||||
|
||||
_deck.sort((a, b) {
|
||||
String srsKey(KanjiItem item) {
|
||||
var key = _mode.toString();
|
||||
if (_mode == QuizMode.reading) {
|
||||
if (item.onyomi.isNotEmpty && item.kunyomi.isNotEmpty) {
|
||||
key += _random.nextBool() ? 'onyomi' : 'kunyomi';
|
||||
} else if (item.onyomi.isNotEmpty) {
|
||||
key += 'onyomi';
|
||||
} else {
|
||||
key += 'kunyomi';
|
||||
int getSrsStage(KanjiItem item) {
|
||||
if (mode == QuizMode.reading) {
|
||||
final onyomiStage = item.srsItems['${QuizMode.reading}onyomi']?.srsStage;
|
||||
final kunyomiStage = item.srsItems['${QuizMode.reading}kunyomi']?.srsStage;
|
||||
|
||||
if (onyomiStage != null && kunyomiStage != null) {
|
||||
return min(onyomiStage, kunyomiStage);
|
||||
}
|
||||
return onyomiStage ?? kunyomiStage ?? 0;
|
||||
}
|
||||
return key;
|
||||
return item.srsItems[mode.toString()]?.srsStage ?? 0;
|
||||
}
|
||||
|
||||
final aSrsItem = a.srsItems[srsKey(a)];
|
||||
final bSrsItem = b.srsItems[srsKey(b)];
|
||||
DateTime getLastAsked(KanjiItem item) {
|
||||
if (mode == QuizMode.reading) {
|
||||
final onyomiLastAsked = item.srsItems['${QuizMode.reading}onyomi']?.lastAsked;
|
||||
final kunyomiLastAsked = item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked;
|
||||
|
||||
final aStage = aSrsItem?.srsStage ?? 0;
|
||||
final bStage = bSrsItem?.srsStage ?? 0;
|
||||
if (onyomiLastAsked != null && kunyomiLastAsked != null) {
|
||||
return onyomiLastAsked.isBefore(kunyomiLastAsked)
|
||||
? onyomiLastAsked
|
||||
: kunyomiLastAsked;
|
||||
}
|
||||
return onyomiLastAsked ??
|
||||
kunyomiLastAsked ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
return item.srsItems[mode.toString()]?.lastAsked ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
final aStage = getSrsStage(a);
|
||||
final bStage = getSrsStage(b);
|
||||
|
||||
if (aStage != bStage) {
|
||||
return aStage.compareTo(bStage);
|
||||
}
|
||||
|
||||
final aLastAsked =
|
||||
aSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final bLastAsked =
|
||||
bSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final aLastAsked = getLastAsked(a);
|
||||
final bLastAsked = getLastAsked(b);
|
||||
|
||||
if (aLastAsked != bLastAsked) {
|
||||
return aLastAsked.compareTo(bLastAsked);
|
||||
}
|
||||
|
||||
return _random.nextDouble().compareTo(_random.nextDouble());
|
||||
return aLastAsked.compareTo(bLastAsked);
|
||||
});
|
||||
|
||||
_current = _deck.first;
|
||||
quizState.current = _deck.first;
|
||||
quizState.key = UniqueKey();
|
||||
|
||||
_correctAnswers = [];
|
||||
_options = [];
|
||||
_readingHint = '';
|
||||
quizState.correctAnswers = [];
|
||||
quizState.options = [];
|
||||
quizState.readingHint = '';
|
||||
quizState.selectedOption = null;
|
||||
quizState.showResult = false;
|
||||
|
||||
switch (_mode) {
|
||||
switch (mode) {
|
||||
case QuizMode.kanjiToEnglish:
|
||||
_correctAnswers = [_current!.meanings.first];
|
||||
_options = [
|
||||
_correctAnswers.first,
|
||||
..._dg.generateMeanings(_current!, _deck, 3)
|
||||
quizState.correctAnswers = [quizState.current!.meanings.first];
|
||||
quizState.options = [
|
||||
quizState.correctAnswers.first,
|
||||
..._dg.generateMeanings(quizState.current!, _deck, 3)
|
||||
].map(_toTitleCase).toList()
|
||||
..shuffle();
|
||||
break;
|
||||
|
||||
case QuizMode.englishToKanji:
|
||||
_correctAnswers = [_current!.characters];
|
||||
_options = [
|
||||
_correctAnswers.first,
|
||||
..._dg.generateKanji(_current!, _deck, 3)
|
||||
quizState.correctAnswers = [quizState.current!.characters];
|
||||
quizState.options = [
|
||||
quizState.correctAnswers.first,
|
||||
..._dg.generateKanji(quizState.current!, _deck, 3)
|
||||
]..shuffle();
|
||||
break;
|
||||
|
||||
case QuizMode.reading:
|
||||
final info = _pickReading(_current!);
|
||||
_correctAnswers = info.correctReadings;
|
||||
_readingHint = info.hint;
|
||||
final info = _pickReading(quizState.current!);
|
||||
quizState.correctAnswers = info.correctReadings;
|
||||
quizState.readingHint = info.hint;
|
||||
|
||||
final readingsSource = _readingHint.contains("on'yomi")
|
||||
final readingsSource = quizState.readingHint.contains("on'yomi")
|
||||
? _deck.expand((k) => k.onyomi)
|
||||
: _deck.expand((k) => k.kunyomi);
|
||||
|
||||
final distractors = readingsSource
|
||||
.where((r) => !_correctAnswers.contains(r))
|
||||
.where((r) => !quizState.correctAnswers.contains(r))
|
||||
.toSet()
|
||||
.toList()
|
||||
..shuffle();
|
||||
_options = ([
|
||||
_correctAnswers[_random.nextInt(_correctAnswers.length)],
|
||||
..shuffle();
|
||||
quizState.options = ([
|
||||
quizState.correctAnswers[_random.nextInt(quizState.correctAnswers.length)],
|
||||
...distractors.take(3)
|
||||
])
|
||||
..shuffle();
|
||||
break;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
setState(() {
|
||||
_isAnswering = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _answer(String option) async {
|
||||
final isCorrect = _correctAnswers
|
||||
final quizState = _currentQuizState;
|
||||
final mode = _modeForIndex(_tabController.index);
|
||||
final isCorrect = quizState.correctAnswers
|
||||
.map((a) => a.toLowerCase().trim())
|
||||
.contains(option.toLowerCase().trim());
|
||||
|
||||
final repo = Provider.of<DeckRepository>(context, listen: false);
|
||||
final current = _current!;
|
||||
final current = quizState.current!;
|
||||
|
||||
String readingType = '';
|
||||
if (_mode == QuizMode.reading) {
|
||||
readingType = _readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
|
||||
if (mode == QuizMode.reading) {
|
||||
readingType = quizState.readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
|
||||
}
|
||||
final srsKey = _mode.toString() + readingType;
|
||||
final srsKey = mode.toString() + readingType;
|
||||
|
||||
var srsItem = current.srsItems[srsKey];
|
||||
final isNew = srsItem == null;
|
||||
final srsItemForUpdate = srsItem ??=
|
||||
SrsItem(kanjiId: current.id, quizMode: _mode, readingType: readingType);
|
||||
setState(() {
|
||||
_asked += 1;
|
||||
if (isCorrect) {
|
||||
_score += 1;
|
||||
srsItemForUpdate.srsStage += 1;
|
||||
if (_playCorrectSound) {
|
||||
_audioPlayer.play(AssetSource('sfx/confirm.mp3'));
|
||||
}
|
||||
} else {
|
||||
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
|
||||
SrsItem(kanjiId: current.id, quizMode: mode, readingType: readingType);
|
||||
|
||||
quizState.asked += 1;
|
||||
|
||||
quizState.selectedOption = option;
|
||||
|
||||
quizState.showResult = true;
|
||||
|
||||
setState(() {}); // Trigger UI rebuild to show selected/correct colors
|
||||
|
||||
|
||||
|
||||
if (isCorrect) {
|
||||
quizState.score += 1;
|
||||
srsItemForUpdate.srsStage += 1;
|
||||
if (_playCorrectSound) {
|
||||
_audioPlayer.play(AssetSource('sfx/confirm.mp3'));
|
||||
}
|
||||
srsItemForUpdate.lastAsked = DateTime.now();
|
||||
current.srsItems[srsKey] = srsItemForUpdate;
|
||||
});
|
||||
} else {
|
||||
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
|
||||
}
|
||||
srsItemForUpdate.lastAsked = DateTime.now();
|
||||
current.srsItems[srsKey] = srsItemForUpdate;
|
||||
|
||||
if (isNew) {
|
||||
await repo.insertSrsItem(srsItemForUpdate);
|
||||
@@ -276,11 +316,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
await repo.updateSrsItem(srsItemForUpdate);
|
||||
}
|
||||
|
||||
final correctDisplay = (_mode == QuizMode.kanjiToEnglish)
|
||||
? _toTitleCase(_correctAnswers.first)
|
||||
: (_mode == QuizMode.reading
|
||||
? _correctAnswers.join(', ')
|
||||
: _correctAnswers.first);
|
||||
final correctDisplay = (mode == QuizMode.kanjiToEnglish)
|
||||
? _toTitleCase(quizState.correctAnswers.first)
|
||||
: (mode == QuizMode.reading
|
||||
? quizState.correctAnswers.join(', ')
|
||||
: quizState.correctAnswers.first);
|
||||
|
||||
final snack = SnackBar(
|
||||
content: Text(
|
||||
@@ -297,7 +337,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
ScaffoldMessenger.of(context).showSnackBar(snack);
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 900), _nextQuestion);
|
||||
setState(() {
|
||||
_isAnswering = true; // Disable input after showing result
|
||||
});
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 900), () => _nextQuestion());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -326,22 +370,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
);
|
||||
}
|
||||
|
||||
String prompt = '';
|
||||
String subtitle = '';
|
||||
|
||||
switch (_mode) {
|
||||
case QuizMode.kanjiToEnglish:
|
||||
prompt = _current?.characters ?? '';
|
||||
break;
|
||||
case QuizMode.englishToKanji:
|
||||
prompt = _current != null ? _toTitleCase(_current!.meanings.first) : '';
|
||||
break;
|
||||
case QuizMode.reading:
|
||||
prompt = _current?.characters ?? '';
|
||||
subtitle = _readingHint;
|
||||
break;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Kanji Quiz'),
|
||||
@@ -355,62 +383,97 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0xFF121212),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_status,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildQuizPage(0),
|
||||
_buildQuizPage(1),
|
||||
_buildQuizPage(2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuizPage(int index) {
|
||||
final quizState = _quizStates[index];
|
||||
final mode = _modeForIndex(index);
|
||||
|
||||
String prompt = '';
|
||||
String subtitle = '';
|
||||
|
||||
if (quizState.current != null) {
|
||||
switch (mode) {
|
||||
case QuizMode.kanjiToEnglish:
|
||||
prompt = quizState.current!.characters;
|
||||
break;
|
||||
case QuizMode.englishToKanji:
|
||||
prompt = _toTitleCase(quizState.current!.meanings.first);
|
||||
break;
|
||||
case QuizMode.reading:
|
||||
prompt = quizState.current!.characters;
|
||||
subtitle = quizState.readingHint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
key: quizState.key,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_status,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
const CircularProgressIndicator(color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 0,
|
||||
maxWidth: 500,
|
||||
minHeight: 150,
|
||||
),
|
||||
child: KanjiCard(
|
||||
characters: prompt,
|
||||
subtitle: subtitle,
|
||||
backgroundColor: const Color(0xFF1E1E1E),
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
OptionsGrid(
|
||||
options: quizState.options,
|
||||
onSelected: _isAnswering ? (option) {} : _answer,
|
||||
isDisabled: false,
|
||||
selectedOption: null,
|
||||
correctAnswers: [],
|
||||
showResult: false,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Score: ${quizState.score} / ${quizState.asked}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
if (_loading)
|
||||
const CircularProgressIndicator(color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 0,
|
||||
maxWidth: 500,
|
||||
minHeight: 150,
|
||||
),
|
||||
child: KanjiCard(
|
||||
characters: prompt,
|
||||
subtitle: subtitle,
|
||||
backgroundColor: const Color(0xFF1E1E1E),
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
OptionsGrid(
|
||||
options: _options,
|
||||
onSelected: _answer,
|
||||
buttonColor: const Color(0xFF1E1E1E),
|
||||
textColor: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Score: $_score / $_asked',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user