226 lines
7.3 KiB
Dart
226 lines
7.3 KiB
Dart
import 'dart:async';
|
|
import 'package:sqflite/sqflite.dart';
|
|
import '../models/kanji_item.dart';
|
|
import '../models/srs_item.dart';
|
|
import '../api/wk_client.dart';
|
|
import 'database_constants.dart';
|
|
import 'database_helper.dart';
|
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
class DeckRepository {
|
|
String? _apiKey;
|
|
|
|
Future<void> setApiKey(String apiKey) async {
|
|
_apiKey = apiKey;
|
|
await saveApiKey(apiKey);
|
|
}
|
|
|
|
String? get apiKey => _apiKey;
|
|
|
|
Future<void> saveApiKey(String apiKey) async {
|
|
final db = await DatabaseHelper().db;
|
|
await db.insert(DbConstants.settingsTable, {
|
|
DbConstants.keyColumn: 'apiKey',
|
|
DbConstants.valueColumn: apiKey,
|
|
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
|
}
|
|
|
|
Future<String?> loadApiKey() async {
|
|
final db = await DatabaseHelper().db;
|
|
final rows = await db.query(
|
|
DbConstants.settingsTable,
|
|
where: '${DbConstants.keyColumn} = ?',
|
|
whereArgs: ['apiKey'],
|
|
);
|
|
|
|
if (rows.isNotEmpty) {
|
|
_apiKey = rows.first[DbConstants.valueColumn] as String;
|
|
return _apiKey;
|
|
}
|
|
|
|
try {
|
|
final envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
|
if (envApiKey != null && envApiKey.isNotEmpty) {
|
|
await saveApiKey(envApiKey);
|
|
_apiKey = envApiKey;
|
|
return _apiKey;
|
|
}
|
|
} catch (_) {}
|
|
|
|
return null;
|
|
}
|
|
|
|
Future<void> saveKanji(List<KanjiItem> items) async {
|
|
final db = await DatabaseHelper().db;
|
|
final batch = db.batch();
|
|
for (final it in items) {
|
|
batch.insert(DbConstants.kanjiTable, {
|
|
DbConstants.idColumn: it.id,
|
|
DbConstants.levelColumn: it.level,
|
|
DbConstants.charactersColumn: it.characters,
|
|
DbConstants.meaningsColumn: it.meanings.join('|'),
|
|
DbConstants.onyomiColumn: it.onyomi.join('|'),
|
|
DbConstants.kunyomiColumn: it.kunyomi.join('|'),
|
|
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
|
}
|
|
await batch.commit(noResult: true);
|
|
}
|
|
|
|
Future<List<KanjiItem>> loadKanji() async {
|
|
final db = await DatabaseHelper().db;
|
|
final rows = await db.query(DbConstants.kanjiTable);
|
|
final kanjiItems = rows
|
|
.map(
|
|
(r) => KanjiItem(
|
|
id: r[DbConstants.idColumn] as int,
|
|
level: r[DbConstants.levelColumn] as int? ?? 0,
|
|
characters: r[DbConstants.charactersColumn] as String,
|
|
meanings: (r[DbConstants.meaningsColumn] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
onyomi: (r[DbConstants.onyomiColumn] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
kunyomi: (r[DbConstants.kunyomiColumn] as String)
|
|
.split('|')
|
|
.where((s) => s.isNotEmpty)
|
|
.toList(),
|
|
),
|
|
)
|
|
.toList();
|
|
|
|
final srsRows = await db.query(DbConstants.srsItemsTable);
|
|
final srsItemsByKanjiId = <int, List<SrsItem>>{};
|
|
for (final r in srsRows) {
|
|
final srsItem = SrsItem(
|
|
subjectId: r[DbConstants.kanjiIdColumn] as int,
|
|
quizMode: QuizMode.values.firstWhere(
|
|
(e) => e.toString() == r[DbConstants.quizModeColumn] as String,
|
|
),
|
|
readingType: r[DbConstants.readingTypeColumn] as String?,
|
|
srsStage: r[DbConstants.srsStageColumn] as int,
|
|
lastAsked: DateTime.parse(r[DbConstants.lastAskedColumn] as String),
|
|
disabled: (r[DbConstants.disabledColumn] as int? ?? 0) == 1,
|
|
);
|
|
srsItemsByKanjiId.putIfAbsent(srsItem.subjectId, () => []).add(srsItem);
|
|
}
|
|
|
|
for (final item in kanjiItems) {
|
|
final srsItems = srsItemsByKanjiId[item.id] ?? [];
|
|
for (final srsItem in srsItems) {
|
|
final key = srsItem.quizMode.toString() + (srsItem.readingType ?? '');
|
|
item.srsItems[key] = srsItem;
|
|
}
|
|
}
|
|
|
|
return kanjiItems;
|
|
}
|
|
|
|
Future<void> updateSrsItems(List<SrsItem> items) async {
|
|
final db = await DatabaseHelper().db;
|
|
final batch = db.batch();
|
|
for (final item in items) {
|
|
var where =
|
|
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
|
final whereArgs = [item.subjectId, item.quizMode.toString()];
|
|
if (item.readingType != null) {
|
|
where += ' AND ${DbConstants.readingTypeColumn} = ?';
|
|
whereArgs.add(item.readingType!);
|
|
} else {
|
|
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
|
|
}
|
|
|
|
batch.update(
|
|
DbConstants.srsItemsTable,
|
|
{
|
|
DbConstants.srsStageColumn: item.srsStage,
|
|
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
|
|
DbConstants.disabledColumn: item.disabled ? 1 : 0,
|
|
},
|
|
where: where,
|
|
whereArgs: whereArgs,
|
|
);
|
|
}
|
|
await batch.commit(noResult: true);
|
|
}
|
|
|
|
Future<void> updateSrsItem(SrsItem item) async {
|
|
final db = await DatabaseHelper().db;
|
|
var where =
|
|
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ?';
|
|
final whereArgs = [item.subjectId, item.quizMode.toString()];
|
|
if (item.readingType != null) {
|
|
where += ' AND ${DbConstants.readingTypeColumn} = ?';
|
|
whereArgs.add(item.readingType!);
|
|
} else {
|
|
where += ' AND ${DbConstants.readingTypeColumn} IS NULL';
|
|
}
|
|
|
|
await db.update(
|
|
DbConstants.srsItemsTable,
|
|
{
|
|
DbConstants.srsStageColumn: item.srsStage,
|
|
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
|
|
DbConstants.disabledColumn: item.disabled ? 1 : 0,
|
|
},
|
|
where: where,
|
|
whereArgs: whereArgs,
|
|
);
|
|
}
|
|
|
|
Future<void> insertSrsItem(SrsItem item) async {
|
|
final db = await DatabaseHelper().db;
|
|
await db.insert(DbConstants.srsItemsTable, {
|
|
DbConstants.kanjiIdColumn: item.subjectId,
|
|
DbConstants.quizModeColumn: item.quizMode.toString(),
|
|
DbConstants.readingTypeColumn: item.readingType,
|
|
DbConstants.srsStageColumn: item.srsStage,
|
|
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
|
|
DbConstants.disabledColumn: item.disabled ? 1 : 0,
|
|
}, 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;
|
|
}
|
|
}
|