themes and some refractoring

This commit is contained in:
Rene Kievits
2025-10-31 17:18:33 +01:00
parent 4eb488e28c
commit de3501c3e4
15 changed files with 443 additions and 414 deletions

View File

@@ -1,5 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/subject.dart';
import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
class WkClient { class WkClient {
final String apiKey; final String apiKey;
@@ -85,4 +88,13 @@ class WkClient {
} }
return out; return out;
} }
static Subject createSubjectFromMap(Map<String, dynamic> map) {
final String object = map['object'];
if (object == 'kanji') {
return KanjiItem.fromSubject(map);
} else if (object == 'vocabulary') {
return VocabularyItem.fromSubject(map);
}
throw Exception('Unknown subject type: $object');
}
} }

View File

@@ -1,54 +1,24 @@
enum QuizMode { kanjiToEnglish, englishToKanji, reading } import 'subject.dart';
class SrsItem { class KanjiItem extends Subject {
final int kanjiId;
final QuizMode quizMode;
final String? readingType;
int srsStage;
DateTime lastAsked;
SrsItem({
required this.kanjiId,
required this.quizMode,
this.readingType,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}
class KanjiItem {
final int id;
final int level;
final String characters;
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 super.id,
required this.level, required super.level,
required this.characters, required super.characters,
required this.meanings, required super.meanings,
required this.onyomi, required this.onyomi,
required this.kunyomi, required this.kunyomi,
}); });
factory KanjiItem.fromSubject(Map<String, dynamic> subj) { factory KanjiItem.fromSubject(Map<String, dynamic> subj) {
final int id = subj['id'] as int; final commonFields = Subject.parseCommonFields(subj);
final data = subj['data'] as Map<String, dynamic>; final data = commonFields['data'] as Map<String, dynamic>;
final int level = data['level'] as int;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
final List<String> onyomi = <String>[]; final List<String> onyomi = <String>[];
final List<String> kunyomi = <String>[]; final List<String> kunyomi = <String>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
if (data['readings'] != null) { if (data['readings'] != null) {
for (final r in data['readings'] as List) { for (final r in data['readings'] as List) {
final typ = r['type'] as String? ?? ''; final typ = r['type'] as String? ?? '';
@@ -62,10 +32,10 @@ class KanjiItem {
} }
return KanjiItem( return KanjiItem(
id: id, id: commonFields['id'] as int,
level: level, level: commonFields['level'] as int,
characters: characters, characters: commonFields['characters'] as String,
meanings: meanings, meanings: commonFields['meanings'] as List<String>,
onyomi: onyomi, onyomi: onyomi,
kunyomi: kunyomi, kunyomi: kunyomi,
); );
@@ -83,88 +53,3 @@ String _katakanaToHiragana(String input) {
} }
return buf.toString(); return buf.toString();
} }
enum VocabQuizMode { vocabToEnglish, englishToVocab, audioToEnglish }
class VocabSrsItem {
final int vocabId;
final VocabQuizMode quizMode;
int srsStage;
DateTime lastAsked;
VocabSrsItem({
required this.vocabId,
required this.quizMode,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}
class PronunciationAudio {
final String url;
final String gender;
PronunciationAudio({required this.url, required this.gender});
}
class VocabularyItem {
final int id;
final int level;
final String characters;
final List<String> meanings;
final List<String> readings;
final List<PronunciationAudio> pronunciationAudios;
final Map<String, VocabSrsItem> srsItems = {};
VocabularyItem({
required this.id,
required this.level,
required this.characters,
required this.meanings,
required this.readings,
required this.pronunciationAudios,
});
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
final int id = subj['id'] as int;
final data = subj['data'] as Map<String, dynamic>;
final int level = data['level'] as int;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
final List<String> readings = <String>[];
final List<PronunciationAudio> pronunciationAudios = <PronunciationAudio>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
if (data['readings'] != null) {
for (final r in data['readings'] as List) {
readings.add(r['reading'] as String);
}
}
if (data['pronunciation_audios'] != null) {
for (final audio in data['pronunciation_audios'] as List) {
final url = audio['url'] as String?;
final metadata = audio['metadata'] as Map<String, dynamic>?;
final gender = metadata?['gender'] as String?;
if (url != null && gender != null) {
pronunciationAudios.add(PronunciationAudio(url: url, gender: gender));
}
}
}
return VocabularyItem(
id: id,
level: level,
characters: characters,
meanings: meanings,
readings: readings,
pronunciationAudios: pronunciationAudios,
);
}
}

View File

@@ -0,0 +1,17 @@
enum QuizMode { kanjiToEnglish, englishToKanji, reading, vocabToEnglish, englishToVocab, audioToEnglish }
class SrsItem {
final int subjectId;
final QuizMode quizMode;
final String? readingType;
int srsStage;
DateTime lastAsked;
SrsItem({
required this.subjectId,
required this.quizMode,
this.readingType,
this.srsStage = 0,
DateTime? lastAsked,
}) : lastAsked = lastAsked ?? DateTime.now();
}

View File

@@ -0,0 +1,38 @@
import 'srs_item.dart';
abstract class Subject {
final int id;
final int level;
final String characters;
final List<String> meanings;
final Map<String, SrsItem> srsItems = {};
Subject({
required this.id,
required this.level,
required this.characters,
required this.meanings,
});
static Map<String, dynamic> parseCommonFields(Map<String, dynamic> subj) {
final int id = subj['id'] as int;
final data = subj['data'] as Map<String, dynamic>;
final int level = data['level'] as int;
final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[];
if (data['meanings'] != null) {
for (final m in data['meanings'] as List) {
meanings.add((m['meaning'] as String).toLowerCase());
}
}
return {
'id': id,
'level': level,
'characters': characters,
'meanings': meanings,
'data': data,
};
}
}

View File

@@ -0,0 +1,15 @@
import 'kanji_item.dart';
import 'vocabulary_item.dart';
import 'subject.dart';
class SubjectFactory {
static Subject fromMap(Map<String, dynamic> map) {
final String object = map['object'];
if (object == 'kanji') {
return KanjiItem.fromSubject(map);
} else if (object == 'vocabulary') {
return VocabularyItem.fromSubject(map);
}
throw Exception('Unknown subject type: $object');
}
}

View File

@@ -0,0 +1,56 @@
import 'subject.dart';
class PronunciationAudio {
final String url;
final String gender;
PronunciationAudio({required this.url, required this.gender});
}
class VocabularyItem extends Subject {
final List<String> readings;
final List<PronunciationAudio> pronunciationAudios;
VocabularyItem({
required super.id,
required super.level,
required super.characters,
required super.meanings,
required this.readings,
required this.pronunciationAudios,
});
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
final commonFields = Subject.parseCommonFields(subj);
final data = commonFields['data'] as Map<String, dynamic>;
final List<String> readings = <String>[];
final List<PronunciationAudio> pronunciationAudios = <PronunciationAudio>[];
if (data['readings'] != null) {
for (final r in data['readings'] as List) {
readings.add(r['reading'] as String);
}
}
if (data['pronunciation_audios'] != null) {
for (final audio in data['pronunciation_audios'] as List) {
final url = audio['url'] as String?;
final metadata = audio['metadata'] as Map<String, dynamic>?;
final gender = metadata?['gender'] as String?;
if (url != null && gender != null) {
pronunciationAudios.add(PronunciationAudio(url: url, gender: gender));
}
}
}
return VocabularyItem(
id: commonFields['id'] as int,
level: commonFields['level'] as int,
characters: commonFields['characters'] as String,
meanings: commonFields['meanings'] as List<String>,
readings: readings,
pronunciationAudios: pronunciationAudios,
);
}
}

View File

@@ -4,6 +4,8 @@ import 'package:hirameki_srs/src/themes.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/kanji_item.dart'; import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import '../services/deck_repository.dart'; import '../services/deck_repository.dart';
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart'; import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
import '../services/custom_deck_repository.dart'; import '../services/custom_deck_repository.dart';
@@ -264,9 +266,9 @@ class _BrowseScreenState extends State<BrowseScreen>
Widget _buildVocabListTile(VocabularyItem item) { Widget _buildVocabListTile(VocabularyItem item) {
final requiredModes = <String>[ final requiredModes = <String>[
VocabQuizMode.vocabToEnglish.toString(), QuizMode.vocabToEnglish.toString(),
VocabQuizMode.englishToVocab.toString(), QuizMode.englishToVocab.toString(),
VocabQuizMode.audioToEnglish.toString(), QuizMode.audioToEnglish.toString(),
]; ];
int minSrsStage = 9; int minSrsStage = 9;
@@ -422,6 +424,8 @@ class _BrowseScreenState extends State<BrowseScreen>
srsScores['Reading (kunyomi)'] = srsItem.srsStage; srsScores['Reading (kunyomi)'] = srsItem.srsStage;
} }
break; break;
default:
break;
} }
} }
@@ -670,7 +674,8 @@ class _BrowseScreenState extends State<BrowseScreen>
setState(() { setState(() {
if (_selectedItems.length == _customDeck.length) { if (_selectedItems.length == _customDeck.length) {
_selectedItems.clear(); _selectedItems.clear();
} else { }
else {
_selectedItems = List.from(_customDeck); _selectedItems = List.from(_customDeck);
} }
}); });
@@ -968,15 +973,17 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
for (final entry in widget.vocab.srsItems.entries) { for (final entry in widget.vocab.srsItems.entries) {
final srsItem = entry.value; final srsItem = entry.value;
switch (srsItem.quizMode) { switch (srsItem.quizMode) {
case VocabQuizMode.vocabToEnglish: case QuizMode.vocabToEnglish:
srsScores['JP -> EN'] = srsItem.srsStage; srsScores['JP -> EN'] = srsItem.srsStage;
break; break;
case VocabQuizMode.englishToVocab: case QuizMode.englishToVocab:
srsScores['EN -> JP'] = srsItem.srsStage; srsScores['EN -> JP'] = srsItem.srsStage;
break; break;
case VocabQuizMode.audioToEnglish: case QuizMode.audioToEnglish:
srsScores['Audio'] = srsItem.srsStage; srsScores['Audio'] = srsItem.srsStage;
break; break;
default:
break;
} }
} }
@@ -1052,3 +1059,4 @@ void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
}, },
); );
} }

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/kanji_item.dart'; import '../models/kanji_item.dart';
import '../models/srs_item.dart';
import '../services/deck_repository.dart'; import '../services/deck_repository.dart';
import '../services/distractor_generator.dart'; import '../services/distractor_generator.dart';
import '../widgets/kanji_card.dart'; import '../widgets/kanji_card.dart';
@@ -61,9 +62,6 @@ class _HomeScreenState extends State<HomeScreen>
super.initState(); super.initState();
_tabController = TabController(length: 3, vsync: this); _tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() { _tabController.addListener(() {
if (_tabController.indexIsChanging) {
_nextQuestion();
}
setState(() {}); setState(() {});
}); });
_dg = widget.distractorGenerator ?? DistractorGenerator(); _dg = widget.distractorGenerator ?? DistractorGenerator();
@@ -266,6 +264,10 @@ class _HomeScreenState extends State<HomeScreen>
...distractors.take(3), ...distractors.take(3),
])..shuffle(); ])..shuffle();
break; break;
default:
// Handle other QuizMode cases if necessary, or throw an error
// if these modes are not expected in this context.
break;
} }
setState(() { setState(() {
@@ -294,7 +296,7 @@ class _HomeScreenState extends State<HomeScreen>
var srsItem = current.srsItems[srsKey]; var srsItem = current.srsItems[srsKey];
final isNew = srsItem == null; final isNew = srsItem == null;
final srsItemForUpdate = srsItem ??= SrsItem( final srsItemForUpdate = srsItem ??= SrsItem(
kanjiId: current.id, subjectId: current.id,
quizMode: mode, quizMode: mode,
readingType: readingType, readingType: readingType,
); );
@@ -432,6 +434,10 @@ class _HomeScreenState extends State<HomeScreen>
prompt = quizState.current!.characters; prompt = quizState.current!.characters;
subtitle = quizState.readingHint; subtitle = quizState.readingHint;
break; break;
default:
// Handle other QuizMode cases if necessary, or throw an error
// if these modes are not expected in this context.
break;
} }
} }

View File

@@ -3,7 +3,8 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/kanji_item.dart'; import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart'; import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
import '../services/distractor_generator.dart'; import '../services/distractor_generator.dart';
import '../widgets/kanji_card.dart'; import '../widgets/kanji_card.dart';
@@ -53,9 +54,6 @@ class _VocabScreenState extends State<VocabScreen>
super.initState(); super.initState();
_tabController = TabController(length: 3, vsync: this); _tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() { _tabController.addListener(() {
if (_tabController.indexIsChanging) {
_nextQuestion();
}
setState(() {}); setState(() {});
}); });
_loadSettings(); _loadSettings();
@@ -130,16 +128,16 @@ class _VocabScreenState extends State<VocabScreen>
.join(' '); .join(' ');
} }
VocabQuizMode _modeForIndex(int index) { QuizMode _modeForIndex(int index) {
switch (index) { switch (index) {
case 0: case 0:
return VocabQuizMode.vocabToEnglish; return QuizMode.vocabToEnglish;
case 1: case 1:
return VocabQuizMode.englishToVocab; return QuizMode.englishToVocab;
case 2: case 2:
return VocabQuizMode.audioToEnglish; return QuizMode.audioToEnglish;
default: default:
return VocabQuizMode.vocabToEnglish; return QuizMode.vocabToEnglish;
} }
} }
@@ -150,7 +148,7 @@ class _VocabScreenState extends State<VocabScreen>
final mode = _modeForIndex(index ?? _tabController.index); final mode = _modeForIndex(index ?? _tabController.index);
List<VocabularyItem> currentDeckForMode = _deck; List<VocabularyItem> currentDeckForMode = _deck;
if (mode == VocabQuizMode.audioToEnglish) { if (mode == QuizMode.audioToEnglish) {
currentDeckForMode = _deck currentDeckForMode = _deck
.where((item) => item.pronunciationAudios.isNotEmpty) .where((item) => item.pronunciationAudios.isNotEmpty)
.toList(); .toList();
@@ -169,10 +167,10 @@ class _VocabScreenState extends State<VocabScreen>
quizState.shuffledDeck.sort((a, b) { quizState.shuffledDeck.sort((a, b) {
final aSrsItem = final aSrsItem =
a.srsItems[mode.toString()] ?? a.srsItems[mode.toString()] ??
VocabSrsItem(vocabId: a.id, quizMode: mode); SrsItem(subjectId: a.id, quizMode: mode);
final bSrsItem = final bSrsItem =
b.srsItems[mode.toString()] ?? b.srsItems[mode.toString()] ??
VocabSrsItem(vocabId: b.id, quizMode: mode); SrsItem(subjectId: b.id, quizMode: mode);
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage); final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
if (stageComparison != 0) { if (stageComparison != 0) {
return stageComparison; return stageComparison;
@@ -186,18 +184,14 @@ class _VocabScreenState extends State<VocabScreen>
quizState.currentIndex++; quizState.currentIndex++;
quizState.key = UniqueKey(); quizState.key = UniqueKey();
if (mode == VocabQuizMode.audioToEnglish) {
_playCurrentAudio();
}
quizState.correctAnswers = []; quizState.correctAnswers = [];
quizState.options = []; quizState.options = [];
quizState.selectedOption = null; quizState.selectedOption = null;
quizState.showResult = false; quizState.showResult = false;
switch (mode) { switch (mode) {
case VocabQuizMode.vocabToEnglish: case QuizMode.vocabToEnglish:
case VocabQuizMode.audioToEnglish: case QuizMode.audioToEnglish:
quizState.correctAnswers = [quizState.current!.meanings.first]; quizState.correctAnswers = [quizState.current!.meanings.first];
quizState.options = [ quizState.options = [
quizState.correctAnswers.first, quizState.correctAnswers.first,
@@ -205,13 +199,15 @@ class _VocabScreenState extends State<VocabScreen>
].map(_toTitleCase).toList()..shuffle(); ].map(_toTitleCase).toList()..shuffle();
break; break;
case VocabQuizMode.englishToVocab: case QuizMode.englishToVocab:
quizState.correctAnswers = [quizState.current!.characters]; quizState.correctAnswers = [quizState.current!.characters];
quizState.options = [ quizState.options = [
quizState.correctAnswers.first, quizState.correctAnswers.first,
..._dg.generateVocab(quizState.current!, _deck, 3), ..._dg.generateVocab(quizState.current!, _deck, 3),
]..shuffle(); ]..shuffle();
break; break;
default:
break;
} }
setState(() { setState(() {
@@ -219,10 +215,12 @@ class _VocabScreenState extends State<VocabScreen>
}); });
} }
Future<void> _playCurrentAudio() async { Future<void> _playCurrentAudio({bool playOnLoad = false}) async {
final current = _currentQuizState.current; final current = _currentQuizState.current;
if (current == null || current.pronunciationAudios.isEmpty) return; if (current == null || current.pronunciationAudios.isEmpty) return;
if (playOnLoad && !_playAudio) return;
final maleAudios = current.pronunciationAudios.where( final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male', (a) => a.gender == 'male',
); );
@@ -250,7 +248,7 @@ class _VocabScreenState extends State<VocabScreen>
var srsItemNullable = current.srsItems[srsKey]; var srsItemNullable = current.srsItems[srsKey];
final isNew = srsItemNullable == null; final isNew = srsItemNullable == null;
final srsItem = final srsItem =
srsItemNullable ?? VocabSrsItem(vocabId: current.id, quizMode: mode); srsItemNullable ?? SrsItem(subjectId: current.id, quizMode: mode);
quizState.asked += 1; quizState.asked += 1;
quizState.selectedOption = option; quizState.selectedOption = option;
@@ -272,7 +270,7 @@ class _VocabScreenState extends State<VocabScreen>
await repo.updateVocabSrsItem(srsItem); await repo.updateVocabSrsItem(srsItem);
} }
final correctDisplay = (mode == VocabQuizMode.vocabToEnglish) final correctDisplay = (mode == QuizMode.vocabToEnglish)
? _toTitleCase(quizState.correctAnswers.first) ? _toTitleCase(quizState.correctAnswers.first)
: quizState.correctAnswers.first; : quizState.correctAnswers.first;
@@ -281,7 +279,9 @@ class _VocabScreenState extends State<VocabScreen>
content: Text( content: Text(
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
style: TextStyle( style: TextStyle(
color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error, color: isCorrect
? Theme.of(context).colorScheme.tertiary
: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -294,7 +294,7 @@ class _VocabScreenState extends State<VocabScreen>
if (_playCorrectSound) { if (_playCorrectSound) {
await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
} }
if (_playAudio && mode != VocabQuizMode.audioToEnglish) { if (_playAudio) {
final maleAudios = current.pronunciationAudios.where( final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male', (a) => a.gender == 'male',
); );
@@ -330,7 +330,12 @@ class _VocabScreenState extends State<VocabScreen>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('WaniKani API key is not set.', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), Text(
'WaniKani API key is not set.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
@@ -375,26 +380,35 @@ class _VocabScreenState extends State<VocabScreen>
if (quizState.current == null) { if (quizState.current == null) {
promptWidget = const SizedBox.shrink(); promptWidget = const SizedBox.shrink();
} else if (mode == VocabQuizMode.audioToEnglish) { } else if (mode == QuizMode.audioToEnglish) {
promptWidget = IconButton( promptWidget = IconButton(
icon: Icon(Icons.volume_up, color: Theme.of(context).colorScheme.onSurface, size: 64), icon: Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurface,
size: 64,
),
onPressed: _playCurrentAudio, onPressed: _playCurrentAudio,
); );
} else { } else {
String promptText = ''; String promptText = '';
switch (mode) { switch (mode) {
case VocabQuizMode.vocabToEnglish: case QuizMode.vocabToEnglish:
promptText = quizState.current!.characters; promptText = quizState.current!.characters;
break; break;
case VocabQuizMode.englishToVocab: case QuizMode.englishToVocab:
promptText = _toTitleCase(quizState.current!.meanings.first); promptText = _toTitleCase(quizState.current!.meanings.first);
break; break;
case VocabQuizMode.audioToEnglish: case QuizMode.audioToEnglish:
break;
default:
break; break;
} }
promptWidget = Text( promptWidget = Text(
promptText, promptText,
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
fontSize: 48,
color: Theme.of(context).colorScheme.onSurface,
),
); );
} }
@@ -405,9 +419,18 @@ class _VocabScreenState extends State<VocabScreen>
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface))), Expanded(
child: Text(
_status,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
if (_loading) if (_loading)
CircularProgressIndicator(color: Theme.of(context).colorScheme.primary), CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
),
], ],
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
@@ -440,7 +463,9 @@ class _VocabScreenState extends State<VocabScreen>
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Score: ${quizState.score} / ${quizState.asked}', 'Score: ${quizState.score} / ${quizState.asked}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
), ),
], ],
), ),

View File

@@ -23,12 +23,7 @@ class CustomDeckRepository {
} }
Future<void> updateCard(CustomKanjiItem item) async { Future<void> updateCard(CustomKanjiItem item) async {
final deck = await getCustomDeck(); await updateCards([item]);
final index = deck.indexWhere((element) => element.characters == item.characters);
if (index != -1) {
deck[index] = item;
await saveDeck(deck);
}
} }
Future<void> updateCards(List<CustomKanjiItem> itemsToUpdate) async { Future<void> updateCards(List<CustomKanjiItem> itemsToUpdate) async {

View File

@@ -0,0 +1,27 @@
class DbConstants {
static const String settingsTable = 'settings';
static const String kanjiTable = 'kanji';
static const String srsItemsTable = 'srs_items';
static const String vocabularyTable = 'vocabulary';
static const String srsVocabItemsTable = 'srs_vocab_items';
static const String keyColumn = 'key';
static const String valueColumn = 'value';
static const String idColumn = 'id';
static const String levelColumn = 'level';
static const String charactersColumn = 'characters';
static const String meaningsColumn = 'meanings';
static const String onyomiColumn = 'onyomi';
static const String kunyomiColumn = 'kunyomi';
static const String readingsColumn = 'readings';
static const String pronunciationAudiosColumn = 'pronunciation_audios';
static const String kanjiIdColumn = 'kanjiId';
static const String vocabIdColumn = 'vocabId';
static const String quizModeColumn = 'quizMode';
static const String readingTypeColumn = 'readingType';
static const String srsStageColumn = 'srsStage';
static const String lastAskedColumn = 'lastAsked';
}

View File

@@ -0,0 +1,92 @@
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'database_constants.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _db;
factory DatabaseHelper() {
return _instance;
}
DatabaseHelper._internal();
Future<Database> get db async {
if (_db != null) return _db!;
_db = await _openDb();
return _db!;
}
Future<void> close() async {
if (_db != null) {
await _db!.close();
_db = null;
}
}
Future<Database> _openDb() async {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'wanikani_srs.db');
return openDatabase(
path,
version: 7,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE ${DbConstants.kanjiTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.onyomiColumn} TEXT, ${DbConstants.kunyomiColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.settingsTable} (${DbConstants.keyColumn} TEXT PRIMARY KEY, ${DbConstants.valueColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.srsItemsTable} (${DbConstants.kanjiIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.readingTypeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.kanjiIdColumn}, ${DbConstants.quizModeColumn}, ${DbConstants.readingTypeColumn}))''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.vocabularyTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.levelColumn} INTEGER, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.readingsColumn} TEXT, ${DbConstants.pronunciationAudiosColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''',
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute(
'''CREATE TABLE IF NOT EXISTS ${DbConstants.settingsTable} (${DbConstants.keyColumn} TEXT PRIMARY KEY, ${DbConstants.valueColumn} TEXT)''',
);
}
if (oldVersion < 4) {
await db.execute(
'''CREATE TABLE IF NOT EXISTS ${DbConstants.srsItemsTable} (${DbConstants.kanjiIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.readingTypeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.kanjiIdColumn}, ${DbConstants.quizModeColumn}, ${DbConstants.readingTypeColumn}))''',
);
}
if (oldVersion < 5) {
await db.execute(
'''CREATE TABLE IF NOT EXISTS ${DbConstants.vocabularyTable} (${DbConstants.idColumn} INTEGER PRIMARY KEY, ${DbConstants.charactersColumn} TEXT, ${DbConstants.meaningsColumn} TEXT, ${DbConstants.readingsColumn} TEXT)''',
);
await db.execute(
'''CREATE TABLE IF NOT EXISTS ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''',
);
}
if (oldVersion < 6) {
try {
await db.execute(
'ALTER TABLE ${DbConstants.vocabularyTable} ADD COLUMN ${DbConstants.pronunciationAudiosColumn} TEXT',
);
} catch (_) {
// Ignore error, column might already exist
}
}
if (oldVersion < 7) {
try {
await db.execute('ALTER TABLE ${DbConstants.kanjiTable} ADD COLUMN ${DbConstants.levelColumn} INTEGER');
await db.execute('ALTER TABLE ${DbConstants.vocabularyTable} ADD COLUMN ${DbConstants.levelColumn} INTEGER');
} catch (_) {
// Ignore error, column might already exist
}
}
},
);
}
}

View File

@@ -1,14 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '../models/kanji_item.dart'; import '../models/kanji_item.dart';
import '../models/srs_item.dart';
import '../api/wk_client.dart'; import '../api/wk_client.dart';
import 'database_constants.dart';
import 'database_helper.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
class DeckRepository { class DeckRepository {
Database? _db;
String? _apiKey; String? _apiKey;
Future<void> setApiKey(String apiKey) async { Future<void> setApiKey(String apiKey) async {
@@ -18,146 +18,75 @@ class DeckRepository {
String? get apiKey => _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: 7,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, level INTEGER, 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, level INTEGER, characters TEXT, meanings TEXT, readings TEXT, pronunciation_audios 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))''',
);
}
if (oldVersion < 6) {
try {
await db.execute(
'ALTER TABLE vocabulary ADD COLUMN pronunciation_audios TEXT',
);
} catch (_) {
// Ignore error, column might already exist
}
}
if (oldVersion < 7) {
try {
await db.execute('ALTER TABLE kanji ADD COLUMN level INTEGER');
await db.execute('ALTER TABLE vocabulary ADD COLUMN level INTEGER');
} catch (_) {
// Ignore error, column might already exist
}
}
},
);
return _db!;
}
Future<void> saveApiKey(String apiKey) async { Future<void> saveApiKey(String apiKey) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.insert('settings', { await db.insert(DbConstants.settingsTable, {
'key': 'apiKey', DbConstants.keyColumn: 'apiKey',
'value': apiKey, DbConstants.valueColumn: apiKey,
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }
Future<String?> loadApiKey() async { Future<String?> loadApiKey() async {
String? envApiKey; final db = await DatabaseHelper().db;
try {
envApiKey = dotenv.env['WANIKANI_API_KEY'];
} catch (e) {
envApiKey = null;
}
if (envApiKey != null && envApiKey.isNotEmpty) {
_apiKey = envApiKey;
return _apiKey;
}
final db = await _openDb();
final rows = await db.query( final rows = await db.query(
'settings', DbConstants.settingsTable,
where: 'key = ?', where: '${DbConstants.keyColumn} = ?',
whereArgs: ['apiKey'], whereArgs: ['apiKey'],
); );
if (rows.isNotEmpty) { if (rows.isNotEmpty) {
_apiKey = rows.first['value'] as String; _apiKey = rows.first[DbConstants.valueColumn] as String;
return _apiKey; 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; return null;
} }
Future<void> saveKanji(List<KanjiItem> items) async { Future<void> saveKanji(List<KanjiItem> items) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
final batch = db.batch(); final batch = db.batch();
for (final it in items) { for (final it in items) {
batch.insert('kanji', { batch.insert(DbConstants.kanjiTable, {
'id': it.id, DbConstants.idColumn: it.id,
'level': it.level, DbConstants.levelColumn: it.level,
'characters': it.characters, DbConstants.charactersColumn: it.characters,
'meanings': it.meanings.join('|'), DbConstants.meaningsColumn: it.meanings.join('|'),
'onyomi': it.onyomi.join('|'), DbConstants.onyomiColumn: it.onyomi.join('|'),
'kunyomi': it.kunyomi.join('|'), DbConstants.kunyomiColumn: it.kunyomi.join('|'),
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }
await batch.commit(noResult: true); await batch.commit(noResult: true);
} }
Future<List<KanjiItem>> loadKanji() async { Future<List<KanjiItem>> loadKanji() async {
final db = await _openDb(); final db = await DatabaseHelper().db;
final rows = await db.query('kanji'); final rows = await db.query(DbConstants.kanjiTable);
final kanjiItems = rows final kanjiItems = rows
.map( .map(
(r) => KanjiItem( (r) => KanjiItem(
id: r['id'] as int, id: r[DbConstants.idColumn] as int,
level: r['level'] as int? ?? 0, level: r[DbConstants.levelColumn] as int? ?? 0,
characters: r['characters'] as String, characters: r[DbConstants.charactersColumn] as String,
meanings: (r['meanings'] as String) meanings: (r[DbConstants.meaningsColumn] as String)
.split('|') .split('|')
.where((s) => s.isNotEmpty) .where((s) => s.isNotEmpty)
.toList(), .toList(),
onyomi: (r['onyomi'] as String) onyomi: (r[DbConstants.onyomiColumn] as String)
.split('|') .split('|')
.where((s) => s.isNotEmpty) .where((s) => s.isNotEmpty)
.toList(), .toList(),
kunyomi: (r['kunyomi'] as String) kunyomi: (r[DbConstants.kunyomiColumn] as String)
.split('|') .split('|')
.where((s) => s.isNotEmpty) .where((s) => s.isNotEmpty)
.toList(), .toList(),
@@ -165,8 +94,23 @@ class DeckRepository {
) )
.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) { for (final item in kanjiItems) {
final srsItems = await getSrsItems(item.id); final srsItems = srsItemsByKanjiId[item.id] ?? [];
for (final srsItem in srsItems) { for (final srsItem in srsItems) {
final key = srsItem.quizMode.toString() + (srsItem.readingType ?? ''); final key = srsItem.quizMode.toString() + (srsItem.readingType ?? '');
item.srsItems[key] = srsItem; item.srsItems[key] = srsItem;
@@ -176,47 +120,27 @@ class DeckRepository {
return kanjiItems; 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 { Future<void> updateSrsItem(SrsItem item) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.update( await db.update(
'srs_items', DbConstants.srsItemsTable,
{ {
'srsStage': item.srsStage, DbConstants.srsStageColumn: item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(), DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
}, },
where: 'kanjiId = ? AND quizMode = ? AND readingType = ?', where: '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ? AND ${DbConstants.readingTypeColumn} = ?',
whereArgs: [item.kanjiId, item.quizMode.toString(), item.readingType], whereArgs: [item.subjectId, item.quizMode.toString(), item.readingType],
); );
} }
Future<void> insertSrsItem(SrsItem item) async { Future<void> insertSrsItem(SrsItem item) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.insert('srs_items', { await db.insert(DbConstants.srsItemsTable, {
'kanjiId': item.kanjiId, DbConstants.kanjiIdColumn: item.subjectId,
'quizMode': item.quizMode.toString(), DbConstants.quizModeColumn: item.quizMode.toString(),
'readingType': item.readingType, DbConstants.readingTypeColumn: item.readingType,
'srsStage': item.srsStage, DbConstants.srsStageColumn: item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(), DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }

View File

@@ -1,4 +1,5 @@
import '../models/kanji_item.dart'; import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
import 'dart:math'; import 'dart:math';
class DistractorGenerator { class DistractorGenerator {

View File

@@ -1,15 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '../models/kanji_item.dart'; import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import '../api/wk_client.dart'; import '../api/wk_client.dart';
import 'database_helper.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
class VocabDeckRepository { class VocabDeckRepository {
Database? _db;
String? _apiKey; String? _apiKey;
Future<void> setApiKey(String apiKey) async { Future<void> setApiKey(String apiKey) async {
@@ -19,86 +18,15 @@ class VocabDeckRepository {
String? get apiKey => _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: 7,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, level INTEGER, 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, level INTEGER, characters TEXT, meanings TEXT, readings TEXT, pronunciation_audios 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))''',
);
}
if (oldVersion < 6) {
try {
await db.execute(
'ALTER TABLE vocabulary ADD COLUMN pronunciation_audios TEXT',
);
} catch (_) {
// Ignore error, column might already exist
}
}
if (oldVersion < 7) {
try {
await db.execute('ALTER TABLE kanji ADD COLUMN level INTEGER');
await db.execute('ALTER TABLE vocabulary ADD COLUMN level INTEGER');
} catch (_) {
// Ignore error, column might already exist
}
}
},
);
return _db!;
}
Future<void> saveApiKey(String apiKey) async { Future<void> saveApiKey(String apiKey) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.insert('settings', { await db.insert('settings', {
'key': 'apiKey', 'key': 'apiKey',
'value': apiKey, 'value': apiKey,
}, conflictAlgorithm: ConflictAlgorithm.replace); }, conflictAlgorithm: ConflictAlgorithm.replace);
} }
Future<String?> loadApiKey() async { Future<String?> loadApiKey() async {
String? envApiKey; String? envApiKey;
try { try {
@@ -112,7 +40,7 @@ class VocabDeckRepository {
return _apiKey; return _apiKey;
} }
final db = await _openDb(); final db = await DatabaseHelper().db;
final rows = await db.query( final rows = await db.query(
'settings', 'settings',
where: 'key = ?', where: 'key = ?',
@@ -125,17 +53,17 @@ class VocabDeckRepository {
return null; return null;
} }
Future<List<VocabSrsItem>> getVocabSrsItems(int vocabId) async { Future<List<SrsItem>> getVocabSrsItems(int vocabId) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
final rows = await db.query( final rows = await db.query(
'srs_vocab_items', 'srs_vocab_items',
where: 'vocabId = ?', where: 'vocabId = ?',
whereArgs: [vocabId], whereArgs: [vocabId],
); );
return rows.map((r) { return rows.map((r) {
return VocabSrsItem( return SrsItem(
vocabId: r['vocabId'] as int, subjectId: r['vocabId'] as int,
quizMode: VocabQuizMode.values.firstWhere( quizMode: QuizMode.values.firstWhere(
(e) => e.toString() == r['quizMode'] as String, (e) => e.toString() == r['quizMode'] as String,
), ),
srsStage: r['srsStage'] as int, srsStage: r['srsStage'] as int,
@@ -144,8 +72,8 @@ class VocabDeckRepository {
}).toList(); }).toList();
} }
Future<void> updateVocabSrsItem(VocabSrsItem item) async { Future<void> updateVocabSrsItem(SrsItem item) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.update( await db.update(
'srs_vocab_items', 'srs_vocab_items',
{ {
@@ -153,14 +81,14 @@ class VocabDeckRepository {
'lastAsked': item.lastAsked.toIso8601String(), 'lastAsked': item.lastAsked.toIso8601String(),
}, },
where: 'vocabId = ? AND quizMode = ?', where: 'vocabId = ? AND quizMode = ?',
whereArgs: [item.vocabId, item.quizMode.toString()], whereArgs: [item.subjectId, item.quizMode.toString()],
); );
} }
Future<void> insertVocabSrsItem(VocabSrsItem item) async { Future<void> insertVocabSrsItem(SrsItem item) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
await db.insert('srs_vocab_items', { await db.insert('srs_vocab_items', {
'vocabId': item.vocabId, 'vocabId': item.subjectId,
'quizMode': item.quizMode.toString(), 'quizMode': item.quizMode.toString(),
'srsStage': item.srsStage, 'srsStage': item.srsStage,
'lastAsked': item.lastAsked.toIso8601String(), 'lastAsked': item.lastAsked.toIso8601String(),
@@ -168,7 +96,7 @@ class VocabDeckRepository {
} }
Future<void> saveVocabulary(List<VocabularyItem> items) async { Future<void> saveVocabulary(List<VocabularyItem> items) async {
final db = await _openDb(); final db = await DatabaseHelper().db;
final batch = db.batch(); final batch = db.batch();
for (final it in items) { for (final it in items) {
final audios = it.pronunciationAudios final audios = it.pronunciationAudios
@@ -187,7 +115,7 @@ class VocabDeckRepository {
} }
Future<List<VocabularyItem>> loadVocabulary() async { Future<List<VocabularyItem>> loadVocabulary() async {
final db = await _openDb(); final db = await DatabaseHelper().db;
final rows = await db.query('vocabulary'); final rows = await db.query('vocabulary');
final vocabItems = rows.map((r) { final vocabItems = rows.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?; final audiosRaw = r['pronunciation_audios'] as String?;