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 {
final int id;
final String characters;
final List<String> meanings;
final List<String> onyomi;
final List<String> kunyomi;
final Map<String, SrsItem> srsItems = {};
KanjiItem({
required this.id,

View File

@@ -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<String> correctReadings;
@@ -65,11 +65,13 @@ class _HomeScreenState extends State<HomeScreen> {
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<HomeScreen> {
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<HomeScreen> {
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<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(() {
_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<HomeScreen> {
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<HomeScreen> {
backgroundColor: const Color(0xFF1E1E1E),
);
}
}
}

View File

@@ -32,7 +32,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
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 {
final repo = Provider.of<DeckRepository>(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<StartScreen> {
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<StartScreen> {
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)),
),

View File

@@ -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<List<KanjiItem>> 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<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 {