added vocabulary audio reading

This commit is contained in:
Rene Kievits
2025-10-28 06:00:15 +01:00
parent 6dabb9c977
commit a572a6e6fc
3 changed files with 99 additions and 29 deletions

View File

@@ -96,19 +96,27 @@ class VocabSrsItem {
}) : 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 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.characters,
required this.meanings,
required this.readings,
});
VocabularyItem(
{required this.id,
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;
@@ -116,6 +124,7 @@ class VocabularyItem {
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) {
@@ -129,11 +138,26 @@ class VocabularyItem {
}
}
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,
characters: characters,
meanings: meanings,
readings: readings,
);
id: id,
characters: characters,
meanings: meanings,
readings: readings,
pronunciationAudios: pronunciationAudios);
}
}

View File

@@ -57,7 +57,8 @@ class _VocabScreenState extends State<VocabScreen> {
}
var items = await repo.loadVocabulary();
if (items.isEmpty) {
if (items.isEmpty ||
items.every((item) => item.pronunciationAudios.isEmpty)) {
setState(() {
_status = 'Fetching deck...';
});
@@ -149,7 +150,6 @@ class _VocabScreenState extends State<VocabScreen> {
_asked += 1;
if (isCorrect) {
_score += 1;
_audioPlayer.play(AssetSource('sfx/confirm.mp3'));
srsItem.srsStage += 1;
} else {
srsItem.srsStage = max(0, srsItem.srsStage - 1);
@@ -183,7 +183,23 @@ class _VocabScreenState extends State<VocabScreen> {
ScaffoldMessenger.of(context).showSnackBar(snack);
}
Future.delayed(const Duration(milliseconds: 900), _nextQuestion);
if (isCorrect) {
await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
final maleAudios =
current.pronunciationAudios.where((a) => a.gender == 'male');
if (maleAudios.isNotEmpty) {
try {
await _audioPlayer.play(UrlSource(maleAudios.first.url));
} catch (e) {
// Ignore player errors
}
}
await Future.delayed(const Duration(milliseconds: 400));
} else {
await Future.delayed(const Duration(milliseconds: 900));
}
_nextQuestion();
}
@override

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
@@ -23,7 +24,7 @@ class DeckRepository {
_db = await openDatabase(
path,
version: 5,
version: 6,
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''');
@@ -32,7 +33,7 @@ class DeckRepository {
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)''');
'''CREATE TABLE vocabulary (id INTEGER PRIMARY KEY, 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))''');
},
@@ -56,6 +57,13 @@ class DeckRepository {
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
}
}
},
);
@@ -257,6 +265,9 @@ class DeckRepository {
final db = await _openDb();
final batch = db.batch();
for (final it in items) {
final audios = it.pronunciationAudios
.map((a) => {'url': a.url, 'gender': a.gender})
.toList();
batch.insert(
'vocabulary',
{
@@ -264,6 +275,7 @@ class DeckRepository {
'characters': it.characters,
'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
@@ -275,18 +287,36 @@ class DeckRepository {
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(),
))
.map((r) {
final audiosRaw = r['pronunciation_audios'] as String?;
final List<PronunciationAudio> audios = [];
if (audiosRaw != null && audiosRaw.isNotEmpty) {
try {
final decoded = jsonDecode(audiosRaw) as List;
for (final audioData in decoded) {
audios.add(PronunciationAudio(
url: audioData['url'] as String,
gender: audioData['gender'] as String,
));
}
} catch (e) {
// Error decoding, so we'll just have no audio for this item
}
}
return 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(),
pronunciationAudios: audios,
);
})
.toList();
for (final item in vocabItems) {
@@ -336,4 +366,4 @@ class DeckRepository {
await saveVocabulary(items);
return items;
}
}
}