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 setApiKey(String apiKey) async { _apiKey = apiKey; await saveApiKey(apiKey); } String? get apiKey => _apiKey; Future saveApiKey(String apiKey) async { final db = await DatabaseHelper().db; await db.insert(DbConstants.settingsTable, { DbConstants.keyColumn: 'apiKey', DbConstants.valueColumn: apiKey, }, conflictAlgorithm: ConflictAlgorithm.replace); } Future 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 saveKanji(List 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> 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 = >{}; 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 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 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> 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 = {}; for (final a in assignments) { final data = a['data'] as Map; 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; } }