Files
Hirameki-SRS/lib/src/services/deck_repository.dart
2025-10-31 17:18:33 +01:00

187 lines
6.0 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 (e) {
// dotenv is not initialized
}
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),
);
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> updateSrsItem(SrsItem item) async {
final db = await DatabaseHelper().db;
await db.update(
DbConstants.srsItemsTable,
{
DbConstants.srsStageColumn: item.srsStage,
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
},
where: '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ? AND ${DbConstants.readingTypeColumn} = ?',
whereArgs: [item.subjectId, item.quizMode.toString(), item.readingType],
);
}
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(),
}, 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;
}
}