340 lines
11 KiB
Dart
340 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'package:path/path.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:sqflite/sqflite.dart';
|
|
import '../models/kanji_item.dart';
|
|
import '../api/wk_client.dart';
|
|
|
|
class DeckRepository {
|
|
Database? _db;
|
|
String? _apiKey;
|
|
|
|
Future<void> setApiKey(String apiKey) async {
|
|
_apiKey = apiKey;
|
|
await saveApiKey(apiKey);
|
|
}
|
|
|
|
String? get apiKey => _apiKey;
|
|
|
|
Future<Database> _openDb() async {
|
|
if (_db != null) return _db!;
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final path = join(dir.path, 'wanikani_srs.db');
|
|
|
|
_db = await openDatabase(
|
|
path,
|
|
version: 5,
|
|
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))''');
|
|
await db.execute(
|
|
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT)''');
|
|
await db.execute(
|
|
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
|
|
},
|
|
onUpgrade: (db, oldVersion, newVersion) async {
|
|
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.
|
|
}
|
|
if (oldVersion < 5) {
|
|
await db.execute(
|
|
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, readings TEXT)''');
|
|
await db.execute(
|
|
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))''');
|
|
}
|
|
},
|
|
);
|
|
|
|
return _db!;
|
|
}
|
|
|
|
Future<void> saveApiKey(String apiKey) async {
|
|
final db = await _openDb();
|
|
await db.insert(
|
|
'settings',
|
|
{'key': 'apiKey', 'value': apiKey},
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
|
|
Future<String?> loadApiKey() async {
|
|
final db = await _openDb();
|
|
final rows =
|
|
await db.query('settings', where: 'key = ?', whereArgs: ['apiKey']);
|
|
if (rows.isNotEmpty) {
|
|
_apiKey = rows.first['value'] as String;
|
|
return _apiKey;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<void> saveKanji(List<KanjiItem> items) async {
|
|
final db = await _openDb();
|
|
final batch = db.batch();
|
|
for (final it in items) {
|
|
batch.insert(
|
|
'kanji',
|
|
{
|
|
'id': it.id,
|
|
'characters': it.characters,
|
|
'meanings': it.meanings.join('|'),
|
|
'onyomi': it.onyomi.join('|'),
|
|
'kunyomi': it.kunyomi.join('|'),
|
|
},
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
await batch.commit(noResult: true);
|
|
}
|
|
|
|
Future<List<KanjiItem>> loadKanji() async {
|
|
final db = await _openDb();
|
|
final rows = await db.query('kanji');
|
|
final kanjiItems = rows
|
|
.map((r) => KanjiItem(
|
|
id: r['id'] as int,
|
|
characters: r['characters'] as String,
|
|
meanings: (r['meanings'] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
onyomi: (r['onyomi'] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
kunyomi: (r['kunyomi'] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.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 {
|
|
final key = apiKey ?? _apiKey;
|
|
if (key == null) throw Exception('API key not set');
|
|
|
|
final client = WkClient(key);
|
|
final assignments =
|
|
await client.fetchAllAssignments(subjectTypes: ['kanji']);
|
|
|
|
final unlocked = <int>{};
|
|
for (final a in assignments) {
|
|
final data = a['data'] as Map<String, dynamic>;
|
|
final sidRaw = data['subject_id'];
|
|
if (sidRaw == null) continue;
|
|
final sid = sidRaw is int ? sidRaw : int.tryParse(sidRaw.toString());
|
|
if (sid == null) continue;
|
|
final started = data['started_at'];
|
|
final srs = data['srs_stage'];
|
|
final isUnlocked = (started != null) || (srs != null && (srs as int) > 0);
|
|
if (isUnlocked) unlocked.add(sid);
|
|
}
|
|
|
|
if (unlocked.isEmpty) return [];
|
|
|
|
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
|
|
final items = subjects
|
|
.where((s) =>
|
|
s['object'] == 'kanji' ||
|
|
(s['data'] != null &&
|
|
(s['data'] as Map)['object_type'] == 'kanji'))
|
|
.map((s) => KanjiItem.fromSubject(s))
|
|
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
|
|
.toList();
|
|
|
|
await saveKanji(items);
|
|
return items;
|
|
}
|
|
|
|
Future<List<VocabSrsItem>> getVocabSrsItems(int vocabId) async {
|
|
final db = await _openDb();
|
|
final rows = await db.query('srs_vocab_items', where: 'vocabId = ?', whereArgs: [vocabId]);
|
|
return rows.map((r) {
|
|
return VocabSrsItem(
|
|
vocabId: r['vocabId'] as int,
|
|
quizMode: VocabQuizMode.values.firstWhere((e) => e.toString() == r['quizMode'] as String),
|
|
srsStage: r['srsStage'] as int,
|
|
lastAsked: DateTime.parse(r['lastAsked'] as String),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Future<void> updateVocabSrsItem(VocabSrsItem item) async {
|
|
final db = await _openDb();
|
|
await db.update(
|
|
'srs_vocab_items',
|
|
{
|
|
'srsStage': item.srsStage,
|
|
'lastAsked': item.lastAsked.toIso8601String(),
|
|
},
|
|
where: 'vocabId = ? AND quizMode = ?',
|
|
whereArgs: [item.vocabId, item.quizMode.toString()],
|
|
);
|
|
}
|
|
|
|
Future<void> insertVocabSrsItem(VocabSrsItem item) async {
|
|
final db = await _openDb();
|
|
await db.insert(
|
|
'srs_vocab_items',
|
|
{
|
|
'vocabId': item.vocabId,
|
|
'quizMode': item.quizMode.toString(),
|
|
'srsStage': item.srsStage,
|
|
'lastAsked': item.lastAsked.toIso8601String(),
|
|
},
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
|
|
Future<void> saveVocabulary(List<VocabularyItem> items) async {
|
|
final db = await _openDb();
|
|
final batch = db.batch();
|
|
for (final it in items) {
|
|
batch.insert(
|
|
'vocabulary',
|
|
{
|
|
'id': it.id,
|
|
'characters': it.characters,
|
|
'meanings': it.meanings.join('|'),
|
|
'readings': it.readings.join('|'),
|
|
},
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
await batch.commit(noResult: true);
|
|
}
|
|
|
|
Future<List<VocabularyItem>> loadVocabulary() async {
|
|
final db = await _openDb();
|
|
final rows = await db.query('vocabulary');
|
|
final vocabItems = rows
|
|
.map((r) => VocabularyItem(
|
|
id: r['id'] as int,
|
|
characters: r['characters'] as String,
|
|
meanings: (r['meanings'] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
readings: (r['readings'] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
))
|
|
.toList();
|
|
|
|
for (final item in vocabItems) {
|
|
final srsItems = await getVocabSrsItems(item.id);
|
|
for (final srsItem in srsItems) {
|
|
final key = srsItem.quizMode.toString();
|
|
item.srsItems[key] = srsItem;
|
|
}
|
|
}
|
|
|
|
return vocabItems;
|
|
}
|
|
|
|
Future<List<VocabularyItem>> fetchAndCacheVocabularyFromWk([String? apiKey]) async {
|
|
final key = apiKey ?? _apiKey;
|
|
if (key == null) throw Exception('API key not set');
|
|
|
|
final client = WkClient(key);
|
|
final assignments =
|
|
await client.fetchAllAssignments(subjectTypes: ['vocabulary']);
|
|
|
|
final unlocked = <int>{};
|
|
for (final a in assignments) {
|
|
final data = a['data'] as Map<String, dynamic>;
|
|
final sidRaw = data['subject_id'];
|
|
if (sidRaw == null) continue;
|
|
final sid = sidRaw is int ? sidRaw : int.tryParse(sidRaw.toString());
|
|
if (sid == null) continue;
|
|
final started = data['started_at'];
|
|
final srs = data['srs_stage'];
|
|
final isUnlocked = (started != null) || (srs != null && (srs as int) > 0);
|
|
if (isUnlocked) unlocked.add(sid);
|
|
}
|
|
|
|
if (unlocked.isEmpty) return [];
|
|
|
|
final subjects = await client.fetchSubjectsByIds(unlocked.toList());
|
|
final items = subjects
|
|
.where((s) =>
|
|
s['object'] == 'vocabulary' ||
|
|
(s['data'] != null &&
|
|
(s['data'] as Map)['object_type'] == 'vocabulary'))
|
|
.map((s) => VocabularyItem.fromSubject(s))
|
|
.where((k) => k.characters.isNotEmpty && k.meanings.isNotEmpty)
|
|
.toList();
|
|
|
|
await saveVocabulary(items);
|
|
return items;
|
|
}
|
|
}
|