more cleanup and small fixes, new sound effects

This commit is contained in:
Rene Kievits
2025-11-01 07:50:21 +01:00
parent d5ff5eb12f
commit e9f115a32a
9 changed files with 140 additions and 41 deletions

Binary file not shown.

BIN
assets/sfx/correct.wav Normal file

Binary file not shown.

BIN
assets/sfx/incorrect.wav Normal file

Binary file not shown.

View File

@@ -868,8 +868,9 @@ class _BrowseScreenState extends State<BrowseScreen>
class _VocabDetailsDialog extends StatefulWidget { class _VocabDetailsDialog extends StatefulWidget {
final VocabularyItem vocab; final VocabularyItem vocab;
final ThemeData theme;
const _VocabDetailsDialog({required this.vocab}); const _VocabDetailsDialog({required this.vocab, required this.theme});
@override @override
State<_VocabDetailsDialog> createState() => _VocabDetailsDialogState(); State<_VocabDetailsDialog> createState() => _VocabDetailsDialogState();
@@ -881,10 +882,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchExampleSentences(context); _fetchExampleSentences();
} }
Future<void> _fetchExampleSentences(BuildContext context) async { Future<void> _fetchExampleSentences() async {
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)}',
@@ -911,11 +912,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
children: [ children: [
Text( Text(
japaneseWord, japaneseWord,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(color: widget.theme.colorScheme.onSurface),
), ),
Text( Text(
englishDefinition, englishDefinition,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), style: TextStyle(color: widget.theme.colorScheme.onSurfaceVariant),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@@ -929,7 +930,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
sentences.add( sentences.add(
Text( Text(
'No example sentences found.', '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 = [ _exampleSentences = [
Text( Text(
'Failed to load example sentences.', '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 = [ _exampleSentences = [
Text( Text(
'Error loading example sentences.', '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: [ children: [
Text( Text(
'Level: ${widget.vocab.level}', 'Level: ${widget.vocab.level}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(color: widget.theme.colorScheme.onSurface),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (widget.vocab.meanings.isNotEmpty) if (widget.vocab.meanings.isNotEmpty)
Text( Text(
'Meanings: ${widget.vocab.meanings.join(', ')}', 'Meanings: ${widget.vocab.meanings.join(', ')}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(color: widget.theme.colorScheme.onSurface),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (widget.vocab.readings.isNotEmpty) if (widget.vocab.readings.isNotEmpty)
Text( Text(
'Readings: ${widget.vocab.readings.join(', ')}', 'Readings: ${widget.vocab.readings.join(', ')}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(color: widget.theme.colorScheme.onSurface),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), Divider(color: widget.theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'SRS Scores:', '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( ...srsScores.entries.map(
(entry) => Text( (entry) => Text(
' ${entry.key}: ${entry.value}', ' ${entry.key}: ${entry.value}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface), style: TextStyle(color: widget.theme.colorScheme.onSurface),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant), Divider(color: widget.theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Example Sentences:', 'Example Sentences:',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), style: TextStyle(color: widget.theme.colorScheme.onSurface, fontWeight: FontWeight.bold),
), ),
..._exampleSentences, ..._exampleSentences,
], ],
@@ -1034,22 +1035,23 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
} }
void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) { void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
final currentTheme = Theme.of(context);
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertDialog( return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: currentTheme.colorScheme.surfaceContainer,
title: Text( title: Text(
'Details for ${vocab.characters}', '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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
child: Text( child: Text(
'Close', 'Close',
style: TextStyle(color: Theme.of(context).colorScheme.primary), style: TextStyle(color: currentTheme.colorScheme.primary),
), ),
), ),
], ],

View File

@@ -5,6 +5,8 @@ import '../widgets/options_grid.dart';
import '../widgets/kanji_card.dart'; import '../widgets/kanji_card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/tts_service.dart'; import '../services/tts_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
enum CustomQuizMode { enum CustomQuizMode {
japaneseToEnglish, japaneseToEnglish,
@@ -42,6 +44,11 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
late AnimationController _shakeController; late AnimationController _shakeController;
late Animation<double> _shakeAnimation; late Animation<double> _shakeAnimation;
final List<String> _incorrectlyAnsweredItems = []; final List<String> _incorrectlyAnsweredItems = [];
final _audioPlayer = AudioPlayer();
bool _playIncorrectSound = true;
bool _playCorrectSound = true;
bool _playNarrator = true;
@override @override
void initState() { void initState() {
@@ -58,6 +65,16 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate( _shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn), CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
); );
_loadSettings();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
});
} }
@override @override
@@ -88,7 +105,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
void playAudio() async { void playAudio() async {
if (widget.quizMode == CustomQuizMode.listeningComprehension && if (widget.quizMode == CustomQuizMode.listeningComprehension &&
_currentIndex < _shuffledDeck.length) { _currentIndex < _shuffledDeck.length && _playNarrator) {
final ttsService = Provider.of<TtsService>(context, listen: false); final ttsService = Provider.of<TtsService>(context, listen: false);
await ttsService.speak(_shuffledDeck[_currentIndex].characters); await ttsService.speak(_shuffledDeck[_currentIndex].characters);
} }
@@ -143,8 +160,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
: currentItem.meaning; : currentItem.meaning;
final isCorrect = answer == correctAnswer; final isCorrect = answer == correctAnswer;
int currentSrsLevel = 0; // Initialize with a default value
if (currentItem.useInterval) { if (currentItem.useInterval) {
int currentSrsLevel;
switch (widget.quizMode) { switch (widget.quizMode) {
case CustomQuizMode.japaneseToEnglish: case CustomQuizMode.japaneseToEnglish:
currentSrsLevel = currentItem.srsData.japaneseToEnglish; currentSrsLevel = currentItem.srsData.japaneseToEnglish;
@@ -234,12 +251,55 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
} }
if (isCorrect) { if (isCorrect) {
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 || if (widget.quizMode == CustomQuizMode.japaneseToEnglish ||
widget.quizMode == CustomQuizMode.englishToJapanese) { 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));
} else { } 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); _shakeController.forward(from: 0);
await Future.delayed(const Duration(milliseconds: 900)); await Future.delayed(const Duration(milliseconds: 900));
} }

View File

@@ -54,6 +54,7 @@ class _HomeScreenState extends State<HomeScreen>
final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
_QuizState get _currentQuizState => _quizStates[_tabController.index]; _QuizState get _currentQuizState => _quizStates[_tabController.index];
bool _playIncorrectSound = true;
bool _playCorrectSound = true; bool _playCorrectSound = true;
bool _apiKeyMissing = false; bool _apiKeyMissing = false;
@@ -78,6 +79,7 @@ class _HomeScreenState extends State<HomeScreen>
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
}); });
} }
@@ -311,10 +313,13 @@ class _HomeScreenState extends State<HomeScreen>
quizState.score += 1; quizState.score += 1;
srsItemForUpdate.srsStage += 1; srsItemForUpdate.srsStage += 1;
if (_playCorrectSound) { if (_playCorrectSound) {
_audioPlayer.play(AssetSource('sfx/confirm.mp3')); _audioPlayer.play(AssetSource('sfx/correct.wav'));
} }
} else { } else {
srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1); srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1);
if (_playIncorrectSound) {
_audioPlayer.play(AssetSource('sfx/incorrect.wav'));
}
} }
srsItemForUpdate.lastAsked = DateTime.now(); srsItemForUpdate.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItemForUpdate; current.srsItems[srsKey] = srsItemForUpdate;

View File

@@ -15,8 +15,9 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _apiKeyController = TextEditingController(); final TextEditingController _apiKeyController = TextEditingController();
bool _playAudio = true; bool _playIncorrectSound = true;
bool _playCorrectSound = true; bool _playCorrectSound = true;
bool _playNarrator = true;
@override @override
void dispose() { void dispose() {
@@ -39,8 +40,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_playAudio = prefs.getBool('playAudio') ?? true; _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
}); });
} }
@@ -108,17 +110,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
SwitchListTile( SwitchListTile(
title: Text( title: Text(
'Play audio for vocabulary', 'Play incorrect sound',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
value: _playAudio, value: _playIncorrectSound,
onChanged: (value) async { onChanged: (value) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
prefs.setBool('playAudio', value); prefs.setBool('playIncorrectSound', value);
setState(() { setState(() {
_playAudio = value; _playIncorrectSound = value;
}); });
}, },
activeThumbColor: Theme.of(context).colorScheme.primary, activeThumbColor: Theme.of(context).colorScheme.primary,
@@ -133,7 +135,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
SwitchListTile( SwitchListTile(
title: Text( title: Text(
'Play sound on correct answer', 'Play correct sound',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
@@ -156,6 +158,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
const SizedBox(height: 12), 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( ListTile(
title: Text( title: Text(
'Theme', 'Theme',

View File

@@ -45,8 +45,9 @@ class _VocabScreenState extends State<VocabScreen>
final _quizStates = [_QuizState(), _QuizState(), _QuizState()]; final _quizStates = [_QuizState(), _QuizState(), _QuizState()];
_QuizState get _currentQuizState => _quizStates[_tabController.index]; _QuizState get _currentQuizState => _quizStates[_tabController.index];
bool _playAudio = true; bool _playIncorrectSound = true;
bool _playCorrectSound = true; bool _playCorrectSound = true;
bool _playNarrator = true;
bool _apiKeyMissing = false; bool _apiKeyMissing = false;
@override @override
@@ -72,8 +73,9 @@ class _VocabScreenState extends State<VocabScreen>
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_playAudio = prefs.getBool('playAudio') ?? true; _playIncorrectSound = prefs.getBool('playIncorrectSound') ?? true;
_playCorrectSound = prefs.getBool('playCorrectSound') ?? true; _playCorrectSound = prefs.getBool('playCorrectSound') ?? true;
_playNarrator = prefs.getBool('playNarrator') ?? true;
}); });
} }
@@ -222,7 +224,7 @@ class _VocabScreenState extends State<VocabScreen>
final current = _currentQuizState.current; final current = _currentQuizState.current;
if (current == null || current.pronunciationAudios.isEmpty) return; if (current == null || current.pronunciationAudios.isEmpty) return;
if (playOnLoad && !_playAudio) return; if (playOnLoad && !_playNarrator) return;
final maleAudios = current.pronunciationAudios.where( final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male', (a) => a.gender == 'male',
@@ -263,6 +265,9 @@ class _VocabScreenState extends State<VocabScreen>
srsItem.srsStage += 1; srsItem.srsStage += 1;
} else { } else {
srsItem.srsStage = max(0, srsItem.srsStage - 1); srsItem.srsStage = max(0, srsItem.srsStage - 1);
if (_playIncorrectSound) {
await _audioPlayer.play(AssetSource('sfx/incorrect.wav'));
}
} }
srsItem.lastAsked = DateTime.now(); srsItem.lastAsked = DateTime.now();
current.srsItems[srsKey] = srsItem; current.srsItems[srsKey] = srsItem;
@@ -294,10 +299,9 @@ class _VocabScreenState extends State<VocabScreen>
ScaffoldMessenger.of(context).showSnackBar(snack); ScaffoldMessenger.of(context).showSnackBar(snack);
if (isCorrect) { if (isCorrect) {
if (_playCorrectSound) { if (_playCorrectSound && !_playNarrator) {
await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); await _audioPlayer.play(AssetSource('sfx/correct.wav'));
} } else if (_playNarrator) {
if (_playAudio) {
final maleAudios = current.pronunciationAudios.where( final maleAudios = current.pronunciationAudios.where(
(a) => a.gender == 'male', (a) => a.gender == 'male',
); );

View File

@@ -34,4 +34,5 @@ flutter_icons:
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/sfx/confirm.mp3 - assets/sfx/correct.wav
- assets/sfx/incorrect.wav