added vocabulary audio reading
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user