some cleanup and some fixes
This commit is contained in:
@@ -5,12 +5,12 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'src/services/deck_repository.dart';
|
import 'src/services/deck_repository.dart';
|
||||||
import 'src/screens/start_screen.dart';
|
import 'src/screens/start_screen.dart';
|
||||||
|
import 'src/services/tts_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
try {
|
try {
|
||||||
await dotenv.load(fileName: ".env");
|
await dotenv.load(fileName: ".env");
|
||||||
// No need to catch because the file is only needed for dev
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
@@ -19,6 +19,14 @@ void main() async {
|
|||||||
Provider<DeckRepository>(create: (_) => DeckRepository()),
|
Provider<DeckRepository>(create: (_) => DeckRepository()),
|
||||||
Provider<VocabDeckRepository>(create: (_) => VocabDeckRepository()),
|
Provider<VocabDeckRepository>(create: (_) => VocabDeckRepository()),
|
||||||
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
|
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
|
||||||
|
Provider<TtsService>(
|
||||||
|
create: (_) {
|
||||||
|
final ttsService = TtsService();
|
||||||
|
ttsService.initTts();
|
||||||
|
return ttsService;
|
||||||
|
},
|
||||||
|
dispose: (_, ttsService) => ttsService.dispose(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: const WkApp(),
|
child: const WkApp(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
final _kanaKit = const KanaKit();
|
final _kanaKit = const KanaKit();
|
||||||
final _deckRepository = CustomDeckRepository();
|
final _deckRepository = CustomDeckRepository();
|
||||||
bool _useInterval = false;
|
bool _useInterval = false;
|
||||||
|
late FocusNode _japaneseFocusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_japaneseController.addListener(_convertToKana);
|
_japaneseController.addListener(_convertToKana);
|
||||||
|
_japaneseFocusNode = FocusNode();
|
||||||
|
_japaneseFocusNode.addListener(_onJapaneseFocusChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,13 +34,24 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
_japaneseController.dispose();
|
_japaneseController.dispose();
|
||||||
_englishController.dispose();
|
_englishController.dispose();
|
||||||
_kanjiController.dispose();
|
_kanjiController.dispose();
|
||||||
|
_japaneseFocusNode.removeListener(_onJapaneseFocusChange);
|
||||||
|
_japaneseFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _convertToKana() {
|
void _convertToKana() {
|
||||||
final text = _japaneseController.text;
|
final text = _japaneseController.text;
|
||||||
|
final selection = _japaneseController.selection;
|
||||||
|
final offset = selection.baseOffset;
|
||||||
|
|
||||||
|
if ((offset > 1 && text[offset - 1] == 'n' && text[offset - 2] != 'n') ||
|
||||||
|
(offset == 1 && text[offset - 1] == 'n')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final converted = _kanaKit.toKana(text);
|
final converted = _kanaKit.toKana(text);
|
||||||
if (text != converted) {
|
|
||||||
|
if (converted != text) {
|
||||||
_japaneseController.value = _japaneseController.value.copyWith(
|
_japaneseController.value = _japaneseController.value.copyWith(
|
||||||
text: converted,
|
text: converted,
|
||||||
selection: TextSelection.fromPosition(
|
selection: TextSelection.fromPosition(
|
||||||
@@ -47,6 +61,21 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onJapaneseFocusChange() {
|
||||||
|
if (!_japaneseFocusNode.hasFocus) {
|
||||||
|
_forceNConversion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _forceNConversion() {
|
||||||
|
final text = _japaneseController.text;
|
||||||
|
if (text.isNotEmpty &&
|
||||||
|
text.endsWith('n') &&
|
||||||
|
_kanaKit.toKana(text) != text) {
|
||||||
|
_japaneseController.text = _kanaKit.toKana(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _saveCard() {
|
void _saveCard() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
final srsData = _useInterval
|
final srsData = _useInterval
|
||||||
@@ -83,6 +112,7 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _japaneseController,
|
controller: _japaneseController,
|
||||||
|
focusNode: _japaneseFocusNode,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Japanese (Kana)',
|
labelText: 'Japanese (Kana)',
|
||||||
hintText: 'Enter Japanese vocabulary or kanji',
|
hintText: 'Enter Japanese vocabulary or kanji',
|
||||||
|
|||||||
@@ -200,14 +200,13 @@ class _BrowseScreenState extends State<BrowseScreen>
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
height: 60,
|
height: 60,
|
||||||
child: SingleChildScrollView(
|
child: Row(
|
||||||
scrollDirection: Axis.horizontal,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
child: Row(
|
children: List.generate(levels.length, (index) {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
final level = levels[index];
|
||||||
children: List.generate(levels.length, (index) {
|
final isSelected = index == currentPage;
|
||||||
final level = levels[index];
|
return Expanded(
|
||||||
final isSelected = index == currentPage;
|
child: Padding(
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -222,14 +221,14 @@ class _BrowseScreenState extends State<BrowseScreen>
|
|||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
shape: const CircleBorder(),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
),
|
),
|
||||||
child: Text(level.toString()),
|
child: Text(level.toString()),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -882,11 +881,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetchExampleSentences();
|
_fetchExampleSentences(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchExampleSentences() async {
|
Future<void> _fetchExampleSentences(BuildContext context) async {
|
||||||
final theme = Theme.of(context);
|
|
||||||
try {
|
try {
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}',
|
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}',
|
||||||
@@ -913,11 +911,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
japaneseWord,
|
japaneseWord,
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
englishDefinition,
|
englishDefinition,
|
||||||
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
@@ -931,7 +929,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
sentences.add(
|
sentences.add(
|
||||||
Text(
|
Text(
|
||||||
'No example sentences found.',
|
'No example sentences found.',
|
||||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -946,7 +944,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
_exampleSentences = [
|
_exampleSentences = [
|
||||||
Text(
|
Text(
|
||||||
'Failed to load example sentences.',
|
'Failed to load example sentences.',
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -958,7 +956,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
_exampleSentences = [
|
_exampleSentences = [
|
||||||
Text(
|
Text(
|
||||||
'Error loading example sentences.',
|
'Error loading example sentences.',
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter_tts/flutter_tts.dart';
|
|
||||||
import '../models/custom_kanji_item.dart';
|
import '../models/custom_kanji_item.dart';
|
||||||
import '../widgets/options_grid.dart';
|
import '../widgets/options_grid.dart';
|
||||||
import '../widgets/kanji_card.dart';
|
import '../widgets/kanji_card.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../services/tts_service.dart';
|
||||||
|
|
||||||
enum CustomQuizMode {
|
enum CustomQuizMode {
|
||||||
japaneseToEnglish,
|
japaneseToEnglish,
|
||||||
@@ -38,7 +39,6 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
List<String> _options = [];
|
List<String> _options = [];
|
||||||
bool _answered = false;
|
bool _answered = false;
|
||||||
bool? _correct;
|
bool? _correct;
|
||||||
late FlutterTts _flutterTts;
|
|
||||||
late AnimationController _shakeController;
|
late AnimationController _shakeController;
|
||||||
late Animation<double> _shakeAnimation;
|
late Animation<double> _shakeAnimation;
|
||||||
final List<String> _incorrectlyAnsweredItems = [];
|
final List<String> _incorrectlyAnsweredItems = [];
|
||||||
@@ -47,7 +47,6 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_shuffledDeck = widget.deck.toList()..shuffle();
|
_shuffledDeck = widget.deck.toList()..shuffle();
|
||||||
_initTts();
|
|
||||||
if (_shuffledDeck.isNotEmpty) {
|
if (_shuffledDeck.isNotEmpty) {
|
||||||
_generateOptions();
|
_generateOptions();
|
||||||
}
|
}
|
||||||
@@ -61,6 +60,11 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(CustomQuizScreen oldWidget) {
|
void didUpdateWidget(CustomQuizScreen oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@@ -82,21 +86,18 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void playAudio() {
|
void playAudio() async {
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
||||||
_currentIndex < _shuffledDeck.length) {
|
_currentIndex < _shuffledDeck.length) {
|
||||||
_speak(_shuffledDeck[_currentIndex].characters);
|
final ttsService = Provider.of<TtsService>(context, listen: false);
|
||||||
|
await ttsService.speak(_shuffledDeck[_currentIndex].characters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initTts() async {
|
|
||||||
_flutterTts = FlutterTts();
|
|
||||||
await _flutterTts.setLanguage("ja-JP");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_flutterTts.stop();
|
|
||||||
_shakeController.dispose();
|
_shakeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
||||||
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||||
_options = [currentItem.meaning];
|
_options = [currentItem.meaning];
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
_options = [
|
_options = [
|
||||||
widget.useKanji && currentItem.kanji != null
|
widget.useKanji && currentItem.kanji != null
|
||||||
? currentItem.kanji!
|
? currentItem.kanji!
|
||||||
@@ -232,7 +234,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
||||||
|
widget.quizMode == CustomQuizMode.englishToJapanese) {
|
||||||
await _speak(currentItem.characters);
|
await _speak(currentItem.characters);
|
||||||
}
|
}
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
@@ -244,22 +247,24 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
_nextQuestion();
|
_nextQuestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _nextQuestion() {
|
Future<void> _nextQuestion() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIndex++;
|
_currentIndex++;
|
||||||
_answered = false;
|
_answered = false;
|
||||||
_correct = null;
|
_correct = null;
|
||||||
if (_currentIndex < _shuffledDeck.length) {
|
if (_currentIndex < _shuffledDeck.length) {
|
||||||
_generateOptions();
|
_generateOptions();
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
|
||||||
_speak(_shuffledDeck[_currentIndex].characters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (_currentIndex < _shuffledDeck.length &&
|
||||||
|
widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||||
|
await _speak(_shuffledDeck[_currentIndex].characters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _speak(String text) async {
|
Future<void> _speak(String text) async {
|
||||||
await _flutterTts.speak(text);
|
final ttsService = Provider.of<TtsService>(context, listen: false);
|
||||||
|
await ttsService.speak(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onOptionSelected(String option) {
|
void _onOptionSelected(String option) {
|
||||||
@@ -287,6 +292,12 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
icon: const Icon(Icons.volume_up, size: 64),
|
icon: const Icon(Icons.volume_up, size: 64),
|
||||||
onPressed: () => _speak(currentItem.characters),
|
onPressed: () => _speak(currentItem.characters),
|
||||||
);
|
);
|
||||||
|
} else if (widget.quizMode == CustomQuizMode.englishToJapanese) {
|
||||||
|
promptWidget = Text(
|
||||||
|
question,
|
||||||
|
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
promptWidget = GestureDetector(
|
promptWidget = GestureDetector(
|
||||||
onTap: () => _speak(question),
|
onTap: () => _speak(question),
|
||||||
|
|||||||
@@ -265,8 +265,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
])..shuffle();
|
])..shuffle();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle other QuizMode cases if necessary, or throw an error
|
|
||||||
// if these modes are not expected in this context.
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,8 +433,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
subtitle = quizState.readingHint;
|
subtitle = quizState.readingHint;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle other QuizMode cases if necessary, or throw an error
|
|
||||||
// if these modes are not expected in this context.
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ 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.index == 2 && !_tabController.indexIsChanging) {
|
||||||
|
_playCurrentAudio();
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
_loadSettings();
|
_loadSettings();
|
||||||
|
|||||||
@@ -50,43 +50,6 @@ class DatabaseHelper {
|
|||||||
'''CREATE TABLE ${DbConstants.srsVocabItemsTable} (${DbConstants.vocabIdColumn} INTEGER, ${DbConstants.quizModeColumn} TEXT, ${DbConstants.srsStageColumn} INTEGER, ${DbConstants.lastAskedColumn} TEXT, PRIMARY KEY (${DbConstants.vocabIdColumn}, ${DbConstants.quizModeColumn}))''',
|
'''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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ class DeckRepository {
|
|||||||
_apiKey = envApiKey;
|
_apiKey = envApiKey;
|
||||||
return _apiKey;
|
return _apiKey;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_) {}
|
||||||
// dotenv is not initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -128,7 +126,8 @@ class DeckRepository {
|
|||||||
DbConstants.srsStageColumn: item.srsStage,
|
DbConstants.srsStageColumn: item.srsStage,
|
||||||
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
|
DbConstants.lastAskedColumn: item.lastAsked.toIso8601String(),
|
||||||
},
|
},
|
||||||
where: '${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ? AND ${DbConstants.readingTypeColumn} = ?',
|
where:
|
||||||
|
'${DbConstants.kanjiIdColumn} = ? AND ${DbConstants.quizModeColumn} = ? AND ${DbConstants.readingTypeColumn} = ?',
|
||||||
whereArgs: [item.subjectId, item.quizMode.toString(), item.readingType],
|
whereArgs: [item.subjectId, item.quizMode.toString(), item.readingType],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
56
lib/src/services/tts_service.dart
Normal file
56
lib/src/services/tts_service.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter_tts/flutter_tts.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class TtsService {
|
||||||
|
FlutterTts? _flutterTts;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
Future<void> initTts() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
_flutterTts = FlutterTts();
|
||||||
|
if (_flutterTts != null) {
|
||||||
|
final isAvailable = await _flutterTts!.isLanguageAvailable("ja-JP");
|
||||||
|
if (isAvailable == true) {
|
||||||
|
await _flutterTts?.setLanguage("ja-JP");
|
||||||
|
} else {
|
||||||
|
debugPrint('Japanese (ja-JP) TTS language not available.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isLanguageAvailable(String language) async {
|
||||||
|
if (_flutterTts == null) {
|
||||||
|
await initTts();
|
||||||
|
}
|
||||||
|
return await _flutterTts?.isLanguageAvailable(language) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> speak(String text) async {
|
||||||
|
const int maxRetries = 3;
|
||||||
|
for (int i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
if (_flutterTts == null || !_isInitialized) {
|
||||||
|
await initTts();
|
||||||
|
}
|
||||||
|
await _flutterTts?.speak(text);
|
||||||
|
return;
|
||||||
|
} on PlatformException catch (_) {
|
||||||
|
debugPrint('TTS speak failed, retrying...');
|
||||||
|
await _flutterTts?.stop();
|
||||||
|
_flutterTts = null;
|
||||||
|
_isInitialized = false;
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugPrint('Failed to speak after $maxRetries retries.');
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_flutterTts?.stop();
|
||||||
|
_flutterTts = null;
|
||||||
|
_isInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user