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(); }) : lastAsked = lastAsked ?? DateTime.now();
} }
class PronunciationAudio {
final String url;
final String gender;
PronunciationAudio({required this.url, required this.gender});
}
class VocabularyItem { class VocabularyItem {
final int id; final int id;
final String characters; final String characters;
final List<String> meanings; final List<String> meanings;
final List<String> readings; final List<String> readings;
final List<PronunciationAudio> pronunciationAudios;
final Map<String, VocabSrsItem> srsItems = {}; final Map<String, VocabSrsItem> srsItems = {};
VocabularyItem({ VocabularyItem(
required this.id, {required this.id,
required this.characters, required this.characters,
required this.meanings, required this.meanings,
required this.readings, required this.readings,
}); required this.pronunciationAudios});
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) { factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
final int id = subj['id'] as int; final int id = subj['id'] as int;
@@ -116,6 +124,7 @@ class VocabularyItem {
final String characters = (data['characters'] ?? '') as String; final String characters = (data['characters'] ?? '') as String;
final List<String> meanings = <String>[]; final List<String> meanings = <String>[];
final List<String> readings = <String>[]; final List<String> readings = <String>[];
final List<PronunciationAudio> pronunciationAudios = <PronunciationAudio>[];
if (data['meanings'] != null) { if (data['meanings'] != null) {
for (final m in data['meanings'] as List) { 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( return VocabularyItem(
id: id, id: id,
characters: characters, characters: characters,
meanings: meanings, meanings: meanings,
readings: readings, readings: readings,
); pronunciationAudios: pronunciationAudios);
} }
} }

View File

@@ -57,7 +57,8 @@ class _VocabScreenState extends State<VocabScreen> {
} }
var items = await repo.loadVocabulary(); var items = await repo.loadVocabulary();
if (items.isEmpty) { if (items.isEmpty ||
items.every((item) => item.pronunciationAudios.isEmpty)) {
setState(() { setState(() {
_status = 'Fetching deck...'; _status = 'Fetching deck...';
}); });
@@ -149,7 +150,6 @@ class _VocabScreenState extends State<VocabScreen> {
_asked += 1; _asked += 1;
if (isCorrect) { if (isCorrect) {
_score += 1; _score += 1;
_audioPlayer.play(AssetSource('sfx/confirm.mp3'));
srsItem.srsStage += 1; srsItem.srsStage += 1;
} else { } else {
srsItem.srsStage = max(0, srsItem.srsStage - 1); srsItem.srsStage = max(0, srsItem.srsStage - 1);
@@ -183,7 +183,23 @@ class _VocabScreenState extends State<VocabScreen> {
ScaffoldMessenger.of(context).showSnackBar(snack); 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 @override

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
@@ -23,7 +24,7 @@ class DeckRepository {
_db = await openDatabase( _db = await openDatabase(
path, path,
version: 5, version: 6,
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute( await db.execute(
'''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)'''); '''CREATE TABLE kanji (id INTEGER PRIMARY KEY, characters TEXT, meanings TEXT, onyomi TEXT, kunyomi TEXT)''');
@@ -32,7 +33,7 @@ class DeckRepository {
await db.execute( await db.execute(
'''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))'''); '''CREATE TABLE srs_items (kanjiId INTEGER, quizMode TEXT, readingType TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (kanjiId, quizMode, readingType))''');
await db.execute( 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( await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))'''); '''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( await db.execute(
'''CREATE TABLE srs_vocab_items (vocabId INTEGER, quizMode TEXT, srsStage INTEGER, lastAsked TEXT, PRIMARY KEY (vocabId, quizMode))'''); '''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 db = await _openDb();
final batch = db.batch(); final batch = db.batch();
for (final it in items) { for (final it in items) {
final audios = it.pronunciationAudios
.map((a) => {'url': a.url, 'gender': a.gender})
.toList();
batch.insert( batch.insert(
'vocabulary', 'vocabulary',
{ {
@@ -264,6 +275,7 @@ class DeckRepository {
'characters': it.characters, 'characters': it.characters,
'meanings': it.meanings.join('|'), 'meanings': it.meanings.join('|'),
'readings': it.readings.join('|'), 'readings': it.readings.join('|'),
'pronunciation_audios': jsonEncode(audios),
}, },
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
@@ -275,7 +287,23 @@ class DeckRepository {
final db = await _openDb(); final db = await _openDb();
final rows = await db.query('vocabulary'); final rows = await db.query('vocabulary');
final vocabItems = rows final vocabItems = rows
.map((r) => VocabularyItem( .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, id: r['id'] as int,
characters: r['characters'] as String, characters: r['characters'] as String,
meanings: (r['meanings'] as String) meanings: (r['meanings'] as String)
@@ -286,7 +314,9 @@ class DeckRepository {
.split('|') .split('|')
.where((s) => s.isNotEmpty) .where((s) => s.isNotEmpty)
.toList(), .toList(),
)) pronunciationAudios: audios,
);
})
.toList(); .toList();
for (final item in vocabItems) { for (final item in vocabItems) {