some cleanup and some fixes
This commit is contained in:
@@ -18,11 +18,14 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
final _kanaKit = const KanaKit();
|
||||
final _deckRepository = CustomDeckRepository();
|
||||
bool _useInterval = false;
|
||||
late FocusNode _japaneseFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_japaneseController.addListener(_convertToKana);
|
||||
_japaneseFocusNode = FocusNode();
|
||||
_japaneseFocusNode.addListener(_onJapaneseFocusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -31,13 +34,24 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
_japaneseController.dispose();
|
||||
_englishController.dispose();
|
||||
_kanjiController.dispose();
|
||||
_japaneseFocusNode.removeListener(_onJapaneseFocusChange);
|
||||
_japaneseFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _convertToKana() {
|
||||
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);
|
||||
if (text != converted) {
|
||||
|
||||
if (converted != text) {
|
||||
_japaneseController.value = _japaneseController.value.copyWith(
|
||||
text: converted,
|
||||
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() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final srsData = _useInterval
|
||||
@@ -83,6 +112,7 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _japaneseController,
|
||||
focusNode: _japaneseFocusNode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Japanese (Kana)',
|
||||
hintText: 'Enter Japanese vocabulary or kanji',
|
||||
|
||||
@@ -200,14 +200,13 @@ class _BrowseScreenState extends State<BrowseScreen>
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
height: 60,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(levels.length, (index) {
|
||||
final level = levels[index];
|
||||
final isSelected = index == currentPage;
|
||||
return Padding(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(levels.length, (index) {
|
||||
final level = levels[index];
|
||||
final isSelected = index == currentPage;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
@@ -222,14 +221,14 @@ class _BrowseScreenState extends State<BrowseScreen>
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: const CircleBorder(),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
child: Text(level.toString()),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -882,11 +881,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchExampleSentences();
|
||||
_fetchExampleSentences(context);
|
||||
}
|
||||
|
||||
Future<void> _fetchExampleSentences() async {
|
||||
final theme = Theme.of(context);
|
||||
Future<void> _fetchExampleSentences(BuildContext context) async {
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}',
|
||||
@@ -913,11 +911,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
||||
children: [
|
||||
Text(
|
||||
japaneseWord,
|
||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
Text(
|
||||
englishDefinition,
|
||||
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
@@ -931,7 +929,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
||||
sentences.add(
|
||||
Text(
|
||||
'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 = [
|
||||
Text(
|
||||
'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 = [
|
||||
Text(
|
||||
'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 'dart:math';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import '../models/custom_kanji_item.dart';
|
||||
import '../widgets/options_grid.dart';
|
||||
import '../widgets/kanji_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/tts_service.dart';
|
||||
|
||||
enum CustomQuizMode {
|
||||
japaneseToEnglish,
|
||||
@@ -38,7 +39,6 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
List<String> _options = [];
|
||||
bool _answered = false;
|
||||
bool? _correct;
|
||||
late FlutterTts _flutterTts;
|
||||
late AnimationController _shakeController;
|
||||
late Animation<double> _shakeAnimation;
|
||||
final List<String> _incorrectlyAnsweredItems = [];
|
||||
@@ -47,7 +47,6 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shuffledDeck = widget.deck.toList()..shuffle();
|
||||
_initTts();
|
||||
if (_shuffledDeck.isNotEmpty) {
|
||||
_generateOptions();
|
||||
}
|
||||
@@ -61,6 +60,11 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomQuizScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -82,21 +86,18 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void playAudio() {
|
||||
void playAudio() async {
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
||||
_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
|
||||
void dispose() {
|
||||
_flutterTts.stop();
|
||||
_shakeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -106,7 +107,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
||||
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||
_options = [currentItem.meaning];
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
_options = [
|
||||
widget.useKanji && currentItem.kanji != null
|
||||
? currentItem.kanji!
|
||||
@@ -232,7 +234,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
|
||||
widget.quizMode == CustomQuizMode.englishToJapanese) {
|
||||
await _speak(currentItem.characters);
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
@@ -244,22 +247,24 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
_nextQuestion();
|
||||
}
|
||||
|
||||
void _nextQuestion() {
|
||||
Future<void> _nextQuestion() async {
|
||||
setState(() {
|
||||
_currentIndex++;
|
||||
_answered = false;
|
||||
_correct = null;
|
||||
if (_currentIndex < _shuffledDeck.length) {
|
||||
_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 {
|
||||
await _flutterTts.speak(text);
|
||||
final ttsService = Provider.of<TtsService>(context, listen: false);
|
||||
await ttsService.speak(text);
|
||||
}
|
||||
|
||||
void _onOptionSelected(String option) {
|
||||
@@ -287,6 +292,12 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
icon: const Icon(Icons.volume_up, size: 64),
|
||||
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 {
|
||||
promptWidget = GestureDetector(
|
||||
onTap: () => _speak(question),
|
||||
|
||||
@@ -265,8 +265,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
])..shuffle();
|
||||
break;
|
||||
default:
|
||||
// Handle other QuizMode cases if necessary, or throw an error
|
||||
// if these modes are not expected in this context.
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -435,8 +433,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
subtitle = quizState.readingHint;
|
||||
break;
|
||||
default:
|
||||
// Handle other QuizMode cases if necessary, or throw an error
|
||||
// if these modes are not expected in this context.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ class _VocabScreenState extends State<VocabScreen>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.index == 2 && !_tabController.indexIsChanging) {
|
||||
_playCurrentAudio();
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
_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}))''',
|
||||
);
|
||||
},
|
||||
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;
|
||||
return _apiKey;
|
||||
}
|
||||
} catch (e) {
|
||||
// dotenv is not initialized
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +126,8 @@ class DeckRepository {
|
||||
DbConstants.srsStageColumn: item.srsStage,
|
||||
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],
|
||||
);
|
||||
}
|
||||
|
||||
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