From a572a6e6fca6fc64c8683ef88ffa25eec74aaefa Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Tue, 28 Oct 2025 06:00:15 +0100 Subject: [PATCH] added vocabulary audio reading --- lib/src/models/kanji_item.dart | 46 +++++++++++++++----- lib/src/screens/vocab_screen.dart | 22 ++++++++-- lib/src/services/deck_repository.dart | 60 ++++++++++++++++++++------- 3 files changed, 99 insertions(+), 29 deletions(-) diff --git a/lib/src/models/kanji_item.dart b/lib/src/models/kanji_item.dart index 9b2cbce..3ccae88 100644 --- a/lib/src/models/kanji_item.dart +++ b/lib/src/models/kanji_item.dart @@ -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 meanings; final List readings; + final List pronunciationAudios; final Map 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 subj) { final int id = subj['id'] as int; @@ -116,6 +124,7 @@ class VocabularyItem { final String characters = (data['characters'] ?? '') as String; final List meanings = []; final List readings = []; + final List pronunciationAudios = []; 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?; + 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); } } diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index 33cbb1e..68cce40 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -57,7 +57,8 @@ class _VocabScreenState extends State { } 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 { _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 { 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 diff --git a/lib/src/services/deck_repository.dart b/lib/src/services/deck_repository.dart index ab06dba..02fb917 100644 --- a/lib/src/services/deck_repository.dart +++ b/lib/src/services/deck_repository.dart @@ -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 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; } -} +} \ No newline at end of file