scoring done
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -307,4 +348,4 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: const Color(0xFF1E1E1E),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user