scoring done

This commit is contained in:
Rene Kievits
2025-10-28 02:38:44 +01:00
parent 59fde3457d
commit 61081ac8a4
5 changed files with 151 additions and 20 deletions

View File

@@ -1,9 +1,28 @@
enum QuizMode { kanjiToEnglish, englishToKanji, reading }
class SrsItem {
final int kanjiId;
final QuizMode quizMode;
final String? readingType; // 'onyomi' or 'kunyomi'
int srsStage;
DateTime lastAsked;
SrsItem({
required this.kanjiId,
required this.quizMode,
this.readingType,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}
class KanjiItem { class KanjiItem {
final int id; final int id;
final String characters; final String characters;
final List<String> meanings; final List<String> meanings;
final List<String> onyomi; final List<String> onyomi;
final List<String> kunyomi; final List<String> kunyomi;
final Map<String, SrsItem> srsItems = {};
KanjiItem({ KanjiItem({
required this.id, required this.id,

View File

@@ -8,7 +8,7 @@ import '../widgets/kanji_card.dart';
import '../widgets/options_grid.dart'; import '../widgets/options_grid.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
enum QuizMode { kanjiToEnglish, englishToKanji, reading } import '../models/kanji_item.dart';
class _ReadingInfo { class _ReadingInfo {
final List<String> correctReadings; final List<String> correctReadings;
@@ -65,11 +65,13 @@ class _HomeScreenState extends State<HomeScreen> {
return; return;
} }
setState(() { var items = await repo.loadKanji();
_status = 'Fetching deck...'; if (items.isEmpty) {
}); setState(() {
_status = 'Fetching deck...';
final items = await repo.fetchAndCacheFromWk(apiKey); });
items = await repo.fetchAndCacheFromWk(apiKey);
}
setState(() { setState(() {
_deck = items; _deck = items;
@@ -102,14 +104,24 @@ class _HomeScreenState extends State<HomeScreen> {
final pickedType = choices[_random.nextInt(choices.length)]; final pickedType = choices[_random.nextInt(choices.length)];
final readingsList = pickedType == 'onyomi' ? item.onyomi : item.kunyomi; final readingsList = pickedType == 'onyomi' ? item.onyomi : item.kunyomi;
final hint = 'Select the ${pickedType == 'onyomi' ? "on'yomi" : "kunyomi"}'; final hint = 'Select the ${pickedType == 'onyomi' ? "on\'yomi" : "kunyomi"}';
return _ReadingInfo(readingsList, hint); return _ReadingInfo(readingsList, hint);
} }
void _nextQuestion() { void _nextQuestion() {
if (_deck.isEmpty) return; _deck.sort((a, b) {
_current = (_deck..shuffle()).first; final aSrsItem = a.srsItems[_mode.toString()] ?? SrsItem(kanjiId: a.id, quizMode: _mode);
final bSrsItem = b.srsItems[_mode.toString()] ?? SrsItem(kanjiId: b.id, quizMode: _mode);
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
if (stageComparison != 0) {
return stageComparison;
}
return aSrsItem.lastAsked.compareTo(bSrsItem.lastAsked);
});
_current = _deck.first;
_correctAnswers = []; _correctAnswers = [];
_options = []; _options = [];
@@ -150,15 +162,42 @@ class _HomeScreenState extends State<HomeScreen> {
setState(() {}); setState(() {});
} }
void _answer(String option) { void _answer(String option) async {
final isCorrect = _correctAnswers final isCorrect = _correctAnswers
.map((a) => a.toLowerCase().trim()) .map((a) => a.toLowerCase().trim())
.contains(option.toLowerCase().trim()); .contains(option.toLowerCase().trim());
final repo = Provider.of<DeckRepository>(context, listen: false);
final current = _current!;
String readingType = '';
if (_mode == QuizMode.reading) {
readingType = _readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
}
final srsKey = _mode.toString() + readingType;
var srsItem = current.srsItems[srsKey];
final isNew = srsItem == null;
srsItem ??= SrsItem(kanjiId: current.id, quizMode: _mode, readingType: readingType);
setState(() { setState(() {
_asked += 1; _asked += 1;
if (isCorrect) _score += 1; if (isCorrect) {
_score += 1;
srsItem!.srsStage += 1;
} else {
srsItem!.srsStage = max(0, srsItem.srsStage - 1);
}
srsItem.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItem;
}); });
if (isNew) {
await repo.insertSrsItem(srsItem);
} else {
await repo.updateSrsItem(srsItem);
}
final correctDisplay = (_mode == QuizMode.kanjiToEnglish) final correctDisplay = (_mode == QuizMode.kanjiToEnglish)
? _toTitleCase(_correctAnswers.first) ? _toTitleCase(_correctAnswers.first)
: (_mode == QuizMode.reading ? _correctAnswers.join(', ') : _correctAnswers.first); : (_mode == QuizMode.reading ? _correctAnswers.join(', ') : _correctAnswers.first);
@@ -174,7 +213,9 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: const Color(0xFF222222), backgroundColor: const Color(0xFF222222),
duration: const Duration(milliseconds: 900), duration: const Duration(milliseconds: 900),
); );
ScaffoldMessenger.of(context).showSnackBar(snack); if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(snack);
}
Future.delayed(const Duration(milliseconds: 900), _nextQuestion); Future.delayed(const Duration(milliseconds: 900), _nextQuestion);
} }

View File

@@ -32,7 +32,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()), MaterialPageRoute(builder: (_) => HomeScreen()),
); );
} }
} }

View File

@@ -24,6 +24,11 @@ class _StartScreenState extends State<StartScreen> {
Future<void> _checkApiKey() async { Future<void> _checkApiKey() async {
final repo = Provider.of<DeckRepository>(context, listen: false); final repo = Provider.of<DeckRepository>(context, listen: false);
await repo.loadApiKey(); await repo.loadApiKey();
// TODO: Remove this before release. This is for development purposes only.
if (repo.apiKey == null || repo.apiKey!.isEmpty) {
await repo.setApiKey('91932463-60d2-4552-95a7-4c23cf358189');
}
setState(() { setState(() {
_hasApiKey = repo.apiKey != null && repo.apiKey!.isNotEmpty; _hasApiKey = repo.apiKey != null && repo.apiKey!.isNotEmpty;
_loading = false; _loading = false;
@@ -73,7 +78,7 @@ class _StartScreenState extends State<StartScreen> {
onPressed: () { onPressed: () {
if (_hasApiKey) { if (_hasApiKey) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()), MaterialPageRoute(builder: (_) => HomeScreen()),
); );
} else { } else {
Navigator.of(context).push( Navigator.of(context).push(
@@ -84,7 +89,8 @@ class _StartScreenState extends State<StartScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent, backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), padding:
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
), ),

View File

@@ -23,16 +23,29 @@ class DeckRepository {
_db = await openDatabase( _db = await openDatabase(
path, path,
version: 2, version: 4,
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute( await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)'''); '''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''');
await db.execute( await db.execute(
'''CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)'''); '''CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)''');
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
}, },
onUpgrade: (db, oldVersion, newVersion) async { onUpgrade: (db, oldVersion, newVersion) async {
await db.execute( if (oldVersion < 2) {
'''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)'''); await db.execute(
'''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)''');
}
if (oldVersion < 3) {
// Migration from version 2 to 3 was flawed, so we just drop the columns if they exist
}
if (oldVersion < 4) {
await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
// We are not migrating the old srs data, as it was not mode-specific.
// Old columns will be dropped.
}
}, },
); );
@@ -81,7 +94,7 @@ class DeckRepository {
Future<List<KanjiItem>> loadKanji() async { Future<List<KanjiItem>> loadKanji() async {
final db = await _openDb(); final db = await _openDb();
final rows = await db.query('kanji'); final rows = await db.query('kanji');
return rows final kanjiItems = rows
.map((r) => KanjiItem( .map((r) => KanjiItem(
id: r['id'] as int, id: r['id'] as int,
characters: r['characters'] as String, characters: r['characters'] as String,
@@ -99,6 +112,58 @@ class DeckRepository {
.toList(), .toList(),
)) ))
.toList(); .toList();
for (final item in kanjiItems) {
final srsItems = await getSrsItems(item.id);
for (final srsItem in srsItems) {
final key = srsItem.quizMode.toString() + (srsItem.readingType ?? '');
item.srsItems[key] = srsItem;
}
}
return kanjiItems;
}
Future<List<SrsItem>> getSrsItems(int kanjiId) async {
final db = await _openDb();
final rows = await db.query('srs_items', where: 'kanjiId = ?', whereArgs: [kanjiId]);
return rows.map((r) {
return SrsItem(
kanjiId: r['kanjiId'] as int,
quizMode: QuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
readingType: r['readingType'] as String?,
srsStage: r['srsStage'] as int,
lastAsked: DateTime.parse(r['lastAsked'] as String),
);
}).toList();
}
Future<void> updateSrsItem(SrsItem item) async {
final db = await _openDb();
await db.update(
'srs_items',
{
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
where: 'kanjiId = ? AND quizMode = ? AND readingType = ?',
whereArgs: [item.kanjiId, item.quizMode.toString(), item.readingType],
);
}
Future<void> insertSrsItem(SrsItem item) async {
final db = await _openDb();
await db.insert(
'srs_items',
{
'kanjiId': item.kanjiId,
'quizMode': item.quizMode.toString(),
'readingType': item.readingType,
'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
} }
Future<List<KanjiItem>> fetchAndCacheFromWk([String? apiKey]) async { Future<List<KanjiItem>> fetchAndCacheFromWk([String? apiKey]) async {