diff --git a/assets/sfx/confirm.mp3 b/assets/sfx/confirm.mp3 deleted file mode 100644 index 7cfc0a8..0000000 Binary files a/assets/sfx/confirm.mp3 and /dev/null differ diff --git a/assets/sfx/correct.wav b/assets/sfx/correct.wav new file mode 100644 index 0000000..604564f Binary files /dev/null and b/assets/sfx/correct.wav differ diff --git a/assets/sfx/incorrect.wav b/assets/sfx/incorrect.wav new file mode 100644 index 0000000..79ac2c6 Binary files /dev/null and b/assets/sfx/incorrect.wav differ diff --git a/lib/src/screens/browse_screen.dart b/lib/src/screens/browse_screen.dart index 51d7d53..bfcf510 100644 --- a/lib/src/screens/browse_screen.dart +++ b/lib/src/screens/browse_screen.dart @@ -868,8 +868,9 @@ class _BrowseScreenState extends State class _VocabDetailsDialog extends StatefulWidget { final VocabularyItem vocab; + final ThemeData theme; - const _VocabDetailsDialog({required this.vocab}); + const _VocabDetailsDialog({required this.vocab, required this.theme}); @override State<_VocabDetailsDialog> createState() => _VocabDetailsDialogState(); @@ -881,10 +882,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { @override void initState() { super.initState(); - _fetchExampleSentences(context); + _fetchExampleSentences(); } - Future _fetchExampleSentences(BuildContext context) async { + Future _fetchExampleSentences() async { try { final uri = Uri.parse( 'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}', @@ -911,11 +912,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { children: [ Text( japaneseWord, - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), Text( englishDefinition, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle(color: widget.theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), ], @@ -929,7 +930,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { sentences.add( Text( 'No example sentences found.', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), ); } @@ -944,7 +945,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { _exampleSentences = [ Text( 'Failed to load example sentences.', - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle(color: widget.theme.colorScheme.error), ), ]; }); @@ -956,7 +957,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { _exampleSentences = [ Text( 'Error loading example sentences.', - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle(color: widget.theme.colorScheme.error), ), ]; }); @@ -992,39 +993,39 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { children: [ Text( 'Level: ${widget.vocab.level}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), const SizedBox(height: 16), if (widget.vocab.meanings.isNotEmpty) Text( 'Meanings: ${widget.vocab.meanings.join(', ')}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), const SizedBox(height: 16), if (widget.vocab.readings.isNotEmpty) Text( 'Readings: ${widget.vocab.readings.join(', ')}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), const SizedBox(height: 16), - Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), + Divider(color: widget.theme.colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( 'SRS Scores:', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), + style: TextStyle(color: widget.theme.colorScheme.onSurface, fontWeight: FontWeight.bold), ), ...srsScores.entries.map( (entry) => Text( ' ${entry.key}: ${entry.value}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: widget.theme.colorScheme.onSurface), ), ), const SizedBox(height: 16), - Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), + Divider(color: widget.theme.colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( 'Example Sentences:', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), + style: TextStyle(color: widget.theme.colorScheme.onSurface, fontWeight: FontWeight.bold), ), ..._exampleSentences, ], @@ -1034,22 +1035,23 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> { } void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) { + final currentTheme = Theme.of(context); showDialog( context: context, builder: (dialogContext) { return AlertDialog( - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: currentTheme.colorScheme.surfaceContainer, title: Text( 'Details for ${vocab.characters}', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: currentTheme.colorScheme.onSurface), ), - content: _VocabDetailsDialog(vocab: vocab), + content: _VocabDetailsDialog(vocab: vocab, theme: currentTheme), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: Text( 'Close', - style: TextStyle(color: Theme.of(context).colorScheme.primary), + style: TextStyle(color: currentTheme.colorScheme.primary), ), ), ], diff --git a/lib/src/screens/custom_quiz_screen.dart b/lib/src/screens/custom_quiz_screen.dart index 44f6d55..306dbad 100644 --- a/lib/src/screens/custom_quiz_screen.dart +++ b/lib/src/screens/custom_quiz_screen.dart @@ -5,6 +5,8 @@ import '../widgets/options_grid.dart'; import '../widgets/kanji_card.dart'; import 'package:provider/provider.dart'; import '../services/tts_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:audioplayers/audioplayers.dart'; enum CustomQuizMode { japaneseToEnglish, @@ -42,6 +44,11 @@ class CustomQuizScreenState extends State late AnimationController _shakeController; late Animation _shakeAnimation; final List _incorrectlyAnsweredItems = []; + final _audioPlayer = AudioPlayer(); + + bool _playIncorrectSound = true; + bool _playCorrectSound = true; + bool _playNarrator = true; @override void initState() { @@ -58,6 +65,16 @@ class CustomQuizScreenState extends State _shakeAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn), ); + _loadSettings(); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true; + _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + _playNarrator = prefs.getBool('playNarrator') ?? true; + }); } @override @@ -88,7 +105,7 @@ class CustomQuizScreenState extends State void playAudio() async { if (widget.quizMode == CustomQuizMode.listeningComprehension && - _currentIndex < _shuffledDeck.length) { + _currentIndex < _shuffledDeck.length && _playNarrator) { final ttsService = Provider.of(context, listen: false); await ttsService.speak(_shuffledDeck[_currentIndex].characters); } @@ -143,8 +160,8 @@ class CustomQuizScreenState extends State : currentItem.meaning; final isCorrect = answer == correctAnswer; + int currentSrsLevel = 0; // Initialize with a default value if (currentItem.useInterval) { - int currentSrsLevel; switch (widget.quizMode) { case CustomQuizMode.japaneseToEnglish: currentSrsLevel = currentItem.srsData.japaneseToEnglish; @@ -234,12 +251,55 @@ class CustomQuizScreenState extends State } if (isCorrect) { - if (widget.quizMode == CustomQuizMode.japaneseToEnglish || - widget.quizMode == CustomQuizMode.englishToJapanese) { - await _speak(currentItem.characters); + if (_incorrectlyAnsweredItems.contains(currentItem.characters)) { + _incorrectlyAnsweredItems.remove(currentItem.characters); + } else { + currentSrsLevel++; + } + final interval = pow(2, currentSrsLevel).toInt(); + final newNextReview = DateTime.now().add(Duration(hours: interval)); + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + currentItem.srsData.japaneseToEnglishNextReview = newNextReview; + break; + case CustomQuizMode.englishToJapanese: + currentItem.srsData.englishToJapaneseNextReview = newNextReview; + break; + case CustomQuizMode.listeningComprehension: + currentItem.srsData.listeningComprehensionNextReview = + newNextReview; + break; + } + if (_playCorrectSound && !_playNarrator) { + await _audioPlayer.play(AssetSource('sfx/correct.wav')); + } else if (_playNarrator) { + if (widget.quizMode == CustomQuizMode.japaneseToEnglish || + widget.quizMode == CustomQuizMode.englishToJapanese) { + await _speak(currentItem.characters); + } } await Future.delayed(const Duration(milliseconds: 500)); } else { + if (!_incorrectlyAnsweredItems.contains(currentItem.characters)) { + _incorrectlyAnsweredItems.add(currentItem.characters); + } + currentSrsLevel = max(0, currentSrsLevel - 1); + final newNextReview = DateTime.now().add(const Duration(hours: 1)); + switch (widget.quizMode) { + case CustomQuizMode.japaneseToEnglish: + currentItem.srsData.japaneseToEnglishNextReview = newNextReview; + break; + case CustomQuizMode.englishToJapanese: + currentItem.srsData.englishToJapaneseNextReview = newNextReview; + break; + case CustomQuizMode.listeningComprehension: + currentItem.srsData.listeningComprehensionNextReview = + newNextReview; + break; + } + if (_playIncorrectSound) { + await _audioPlayer.play(AssetSource('sfx/incorrect.wav')); + } _shakeController.forward(from: 0); await Future.delayed(const Duration(milliseconds: 900)); } diff --git a/lib/src/screens/home_screen.dart b/lib/src/screens/home_screen.dart index ce16ec2..17c2fe9 100644 --- a/lib/src/screens/home_screen.dart +++ b/lib/src/screens/home_screen.dart @@ -54,6 +54,7 @@ class _HomeScreenState extends State final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; _QuizState get _currentQuizState => _quizStates[_tabController.index]; + bool _playIncorrectSound = true; bool _playCorrectSound = true; bool _apiKeyMissing = false; @@ -78,6 +79,7 @@ class _HomeScreenState extends State Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); setState(() { + _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; }); } @@ -311,10 +313,13 @@ class _HomeScreenState extends State quizState.score += 1; srsItemForUpdate.srsStage += 1; if (_playCorrectSound) { - _audioPlayer.play(AssetSource('sfx/confirm.mp3')); + _audioPlayer.play(AssetSource('sfx/correct.wav')); } } else { srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1); + if (_playIncorrectSound) { + _audioPlayer.play(AssetSource('sfx/incorrect.wav')); + } } srsItemForUpdate.lastAsked = DateTime.now(); current.srsItems[srsKey] = srsItemForUpdate; diff --git a/lib/src/screens/settings_screen.dart b/lib/src/screens/settings_screen.dart index 25b85e7..c474711 100644 --- a/lib/src/screens/settings_screen.dart +++ b/lib/src/screens/settings_screen.dart @@ -15,8 +15,9 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { final TextEditingController _apiKeyController = TextEditingController(); - bool _playAudio = true; + bool _playIncorrectSound = true; bool _playCorrectSound = true; + bool _playNarrator = true; @override void dispose() { @@ -39,8 +40,9 @@ class _SettingsScreenState extends State { Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); setState(() { - _playAudio = prefs.getBool('playAudio') ?? true; + _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + _playNarrator = prefs.getBool('playNarrator') ?? true; }); } @@ -108,17 +110,17 @@ class _SettingsScreenState extends State { const SizedBox(height: 24), SwitchListTile( title: Text( - 'Play audio for vocabulary', + 'Play incorrect sound', style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), ), - value: _playAudio, + value: _playIncorrectSound, onChanged: (value) async { final prefs = await SharedPreferences.getInstance(); - prefs.setBool('playAudio', value); + prefs.setBool('playIncorrectSound', value); setState(() { - _playAudio = value; + _playIncorrectSound = value; }); }, activeThumbColor: Theme.of(context).colorScheme.primary, @@ -133,7 +135,7 @@ class _SettingsScreenState extends State { const SizedBox(height: 12), SwitchListTile( title: Text( - 'Play sound on correct answer', + 'Play correct sound', style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), @@ -156,6 +158,31 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 12), + SwitchListTile( + title: Text( + 'Play narrator (TTS)', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + value: _playNarrator, + onChanged: (value) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setBool('playNarrator', value); + setState(() { + _playNarrator = value; + }); + }, + activeThumbColor: Theme.of(context).colorScheme.primary, + inactiveThumbColor: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + tileColor: Theme.of(context).colorScheme.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 12), ListTile( title: Text( 'Theme', diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index 9ed43c1..c120f22 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -45,8 +45,9 @@ class _VocabScreenState extends State final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; _QuizState get _currentQuizState => _quizStates[_tabController.index]; - bool _playAudio = true; + bool _playIncorrectSound = true; bool _playCorrectSound = true; + bool _playNarrator = true; bool _apiKeyMissing = false; @override @@ -72,8 +73,9 @@ class _VocabScreenState extends State Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); setState(() { - _playAudio = prefs.getBool('playAudio') ?? true; + _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + _playNarrator = prefs.getBool('playNarrator') ?? true; }); } @@ -222,7 +224,7 @@ class _VocabScreenState extends State final current = _currentQuizState.current; if (current == null || current.pronunciationAudios.isEmpty) return; - if (playOnLoad && !_playAudio) return; + if (playOnLoad && !_playNarrator) return; final maleAudios = current.pronunciationAudios.where( (a) => a.gender == 'male', @@ -263,6 +265,9 @@ class _VocabScreenState extends State srsItem.srsStage += 1; } else { srsItem.srsStage = max(0, srsItem.srsStage - 1); + if (_playIncorrectSound) { + await _audioPlayer.play(AssetSource('sfx/incorrect.wav')); + } } srsItem.lastAsked = DateTime.now(); current.srsItems[srsKey] = srsItem; @@ -294,10 +299,9 @@ class _VocabScreenState extends State ScaffoldMessenger.of(context).showSnackBar(snack); if (isCorrect) { - if (_playCorrectSound) { - await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); - } - if (_playAudio) { + if (_playCorrectSound && !_playNarrator) { + await _audioPlayer.play(AssetSource('sfx/correct.wav')); + } else if (_playNarrator) { final maleAudios = current.pronunciationAudios.where( (a) => a.gender == 'male', ); diff --git a/pubspec.yaml b/pubspec.yaml index 9193382..e13d084 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,4 +34,5 @@ flutter_icons: flutter: uses-material-design: true assets: - - assets/sfx/confirm.mp3 \ No newline at end of file + - assets/sfx/correct.wav + - assets/sfx/incorrect.wav \ No newline at end of file