From 61081ac8a439636c813054c8d66b64247121943c Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Tue, 28 Oct 2025 02:38:44 +0100 Subject: [PATCH] scoring done --- lib/src/models/kanji_item.dart | 19 +++++++ lib/src/screens/home_screen.dart | 67 +++++++++++++++++++----- lib/src/screens/settings_screen.dart | 2 +- lib/src/screens/start_screen.dart | 10 +++- lib/src/services/deck_repository.dart | 73 +++++++++++++++++++++++++-- 5 files changed, 151 insertions(+), 20 deletions(-) diff --git a/lib/src/models/kanji_item.dart b/lib/src/models/kanji_item.dart index 3b57038..2794cdf 100644 --- a/lib/src/models/kanji_item.dart +++ b/lib/src/models/kanji_item.dart @@ -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 { final int id; final String characters; final List meanings; final List onyomi; final List kunyomi; + final Map srsItems = {}; KanjiItem({ required this.id, diff --git a/lib/src/screens/home_screen.dart b/lib/src/screens/home_screen.dart index d8b664d..09356c0 100644 --- a/lib/src/screens/home_screen.dart +++ b/lib/src/screens/home_screen.dart @@ -8,7 +8,7 @@ import '../widgets/kanji_card.dart'; import '../widgets/options_grid.dart'; import 'settings_screen.dart'; -enum QuizMode { kanjiToEnglish, englishToKanji, reading } +import '../models/kanji_item.dart'; class _ReadingInfo { final List correctReadings; @@ -65,11 +65,13 @@ class _HomeScreenState extends State { return; } - setState(() { - _status = 'Fetching deck...'; - }); - - final items = await repo.fetchAndCacheFromWk(apiKey); + var items = await repo.loadKanji(); + if (items.isEmpty) { + setState(() { + _status = 'Fetching deck...'; + }); + items = await repo.fetchAndCacheFromWk(apiKey); + } setState(() { _deck = items; @@ -102,14 +104,24 @@ class _HomeScreenState extends State { final pickedType = choices[_random.nextInt(choices.length)]; 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); } void _nextQuestion() { - if (_deck.isEmpty) return; - _current = (_deck..shuffle()).first; + _deck.sort((a, b) { + 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 = []; _options = []; @@ -150,15 +162,42 @@ class _HomeScreenState extends State { setState(() {}); } - void _answer(String option) { + void _answer(String option) async { final isCorrect = _correctAnswers .map((a) => a.toLowerCase().trim()) .contains(option.toLowerCase().trim()); + + final repo = Provider.of(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(() { _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) ? _toTitleCase(_correctAnswers.first) : (_mode == QuizMode.reading ? _correctAnswers.join(', ') : _correctAnswers.first); @@ -174,7 +213,9 @@ class _HomeScreenState extends State { backgroundColor: const Color(0xFF222222), duration: const Duration(milliseconds: 900), ); - ScaffoldMessenger.of(context).showSnackBar(snack); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(snack); + } Future.delayed(const Duration(milliseconds: 900), _nextQuestion); } @@ -307,4 +348,4 @@ class _HomeScreenState extends State { backgroundColor: const Color(0xFF1E1E1E), ); } -} +} \ No newline at end of file diff --git a/lib/src/screens/settings_screen.dart b/lib/src/screens/settings_screen.dart index 7f261f3..e585eee 100644 --- a/lib/src/screens/settings_screen.dart +++ b/lib/src/screens/settings_screen.dart @@ -32,7 +32,7 @@ class _SettingsScreenState extends State { ); Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const HomeScreen()), + MaterialPageRoute(builder: (_) => HomeScreen()), ); } } diff --git a/lib/src/screens/start_screen.dart b/lib/src/screens/start_screen.dart index bd46302..37ab6de 100644 --- a/lib/src/screens/start_screen.dart +++ b/lib/src/screens/start_screen.dart @@ -24,6 +24,11 @@ class _StartScreenState extends State { Future _checkApiKey() async { final repo = Provider.of(context, listen: false); 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(() { _hasApiKey = repo.apiKey != null && repo.apiKey!.isNotEmpty; _loading = false; @@ -73,7 +78,7 @@ class _StartScreenState extends State { onPressed: () { if (_hasApiKey) { Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const HomeScreen()), + MaterialPageRoute(builder: (_) => HomeScreen()), ); } else { Navigator.of(context).push( @@ -84,7 +89,8 @@ class _StartScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), diff --git a/lib/src/services/deck_repository.dart b/lib/src/services/deck_repository.dart index b4460d9..52973ae 100644 --- a/lib/src/services/deck_repository.dart +++ b/lib/src/services/deck_repository.dart @@ -23,16 +23,29 @@ class DeckRepository { _db = await openDatabase( path, - version: 2, + version: 4, onCreate: (db, version) async { await db.execute( '''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)'''); await db.execute( '''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 { - await db.execute( - '''CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)'''); + if (oldVersion < 2) { + 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> loadKanji() async { final db = await _openDb(); final rows = await db.query('kanji'); - return rows + final kanjiItems = rows .map((r) => KanjiItem( id: r['id'] as int, characters: r['characters'] as String, @@ -99,6 +112,58 @@ class DeckRepository { .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> 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 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 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> fetchAndCacheFromWk([String? apiKey]) async {