diff --git a/lib/src/screens/home_screen.dart b/lib/src/screens/home_screen.dart index 0fac4c4..00813d2 100644 --- a/lib/src/screens/home_screen.dart +++ b/lib/src/screens/home_screen.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../models/kanji_item.dart'; import '../services/deck_repository.dart'; import '../services/distractor_generator.dart'; @@ -39,13 +40,22 @@ class _HomeScreenState extends State { String _readingHint = ''; int _score = 0; int _asked = 0; + bool _playCorrectSound = true; @override void initState() { super.initState(); + _loadSettings(); _loadDeck(); } + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + }); + } + Future _loadDeck() async { setState(() { _loading = true; @@ -112,8 +122,22 @@ class _HomeScreenState extends State { void _nextQuestion() { _deck.sort((a, b) { - final aSrsItem = a.srsItems[_mode.toString()]; - final bSrsItem = b.srsItems[_mode.toString()]; + String srsKey(KanjiItem item) { + var key = _mode.toString(); + if (_mode == QuizMode.reading) { + if (item.onyomi.isNotEmpty && item.kunyomi.isNotEmpty) { + key += _random.nextBool() ? 'onyomi' : 'kunyomi'; + } else if (item.onyomi.isNotEmpty) { + key += 'onyomi'; + } else { + key += 'kunyomi'; + } + } + return key; + } + + final aSrsItem = a.srsItems[srsKey(a)]; + final bSrsItem = b.srsItems[srsKey(b)]; final aStage = aSrsItem?.srsStage ?? 0; final bStage = bSrsItem?.srsStage ?? 0; @@ -122,10 +146,16 @@ class _HomeScreenState extends State { return aStage.compareTo(bStage); } - final aLastAsked = aSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); - final bLastAsked = bSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); + final aLastAsked = + aSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); + final bLastAsked = + bSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); - return aLastAsked.compareTo(bLastAsked); + if (aLastAsked != bLastAsked) { + return aLastAsked.compareTo(bLastAsked); + } + + return _random.nextDouble().compareTo(_random.nextDouble()); }); _current = _deck.first; @@ -199,7 +229,10 @@ class _HomeScreenState extends State { _asked += 1; if (isCorrect) { _score += 1; - _audioPlayer.play(AssetSource('sfx/confirm.mp3')); + srsItemForUpdate.srsStage += 1; + if (_playCorrectSound) { + _audioPlayer.play(AssetSource('sfx/confirm.mp3')); + } } else { srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1); } @@ -265,10 +298,11 @@ class _HomeScreenState extends State { actions: [ IconButton( icon: const Icon(Icons.settings), - onPressed: () { - Navigator.of(context).push( + onPressed: () async { + await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); + _loadSettings(); }, ) ], diff --git a/lib/src/screens/settings_screen.dart b/lib/src/screens/settings_screen.dart index e585eee..2d01a11 100644 --- a/lib/src/screens/settings_screen.dart +++ b/lib/src/screens/settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../services/deck_repository.dart'; import 'home_screen.dart'; @@ -12,6 +13,8 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { final TextEditingController _apiKeyController = TextEditingController(); + bool _playAudio = true; + bool _playCorrectSound = true; @override void dispose() { @@ -19,6 +22,26 @@ class _SettingsScreenState extends State { super.dispose(); } + @override + void initState() { + super.initState(); + _loadSettings(); + final repo = Provider.of(context, listen: false); + repo.loadApiKey().then((key) { + if (key != null) { + _apiKeyController.text = key; + } + }); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _playAudio = prefs.getBool('playAudio') ?? true; + _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + }); + } + Future _saveApiKey() async { final apiKey = _apiKeyController.text.trim(); if (apiKey.isEmpty) return; @@ -32,22 +55,11 @@ class _SettingsScreenState extends State { ); Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => HomeScreen()), + MaterialPageRoute(builder: (_) => const HomeScreen()), ); } } - @override - void initState() { - super.initState(); - final repo = Provider.of(context, listen: false); - repo.loadApiKey().then((key) { - if (key != null) { - _apiKeyController.text = key; - } - }); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -85,6 +97,48 @@ class _SettingsScreenState extends State { ), child: const Text('Save & Start Quiz'), ), + const SizedBox(height: 24), + SwitchListTile( + title: const Text( + 'Play audio for vocabulary', + style: TextStyle(color: Colors.white), + ), + value: _playAudio, + onChanged: (value) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setBool('playAudio', value); + setState(() { + _playAudio = value; + }); + }, + activeThumbColor: Colors.blueAccent, + inactiveThumbColor: Colors.grey, + tileColor: const Color(0xFF1E1E1E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 12), + SwitchListTile( + title: const Text( + 'Play sound on correct answer', + style: TextStyle(color: Colors.white), + ), + value: _playCorrectSound, + onChanged: (value) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setBool('playCorrectSound', value); + setState(() { + _playCorrectSound = value; + }); + }, + activeThumbColor: Colors.blueAccent, + inactiveThumbColor: Colors.grey, + tileColor: const Color(0xFF1E1E1E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), ], ), ), diff --git a/lib/src/screens/vocab_screen.dart b/lib/src/screens/vocab_screen.dart index 68cce40..7cb3ad3 100644 --- a/lib/src/screens/vocab_screen.dart +++ b/lib/src/screens/vocab_screen.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../models/kanji_item.dart'; import '../services/deck_repository.dart'; import '../services/distractor_generator.dart'; @@ -29,13 +31,24 @@ class _VocabScreenState extends State { List _correctAnswers = []; int _score = 0; int _asked = 0; + bool _playAudio = true; + bool _playCorrectSound = true; @override void initState() { super.initState(); + _loadSettings(); _loadDeck(); } + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _playAudio = prefs.getBool('playAudio') ?? true; + _playCorrectSound = prefs.getBool('playCorrectSound') ?? true; + }); + } + Future _loadDeck() async { setState(() { _loading = true; @@ -184,17 +197,28 @@ class _VocabScreenState extends State { } 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 + if (_playCorrectSound) { + await _audioPlayer.play(AssetSource('sfx/confirm.mp3')); + } + if (_playAudio) { + final maleAudios = + current.pronunciationAudios.where((a) => a.gender == 'male'); + if (maleAudios.isNotEmpty) { + final completer = Completer(); + final sub = _audioPlayer.onPlayerComplete.listen((event) { + if (!completer.isCompleted) completer.complete(); + }); + + try { + await _audioPlayer.play(UrlSource(maleAudios.first.url)); + await completer.future.timeout(const Duration(seconds: 5)); + } catch (e) { + // Ignore player errors + } finally { + await sub.cancel(); + } } } - await Future.delayed(const Duration(milliseconds: 400)); } else { await Future.delayed(const Duration(milliseconds: 900)); } @@ -225,10 +249,11 @@ class _VocabScreenState extends State { actions: [ IconButton( icon: const Icon(Icons.settings), - onPressed: () { - Navigator.of(context).push( + onPressed: () async { + await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); + _loadSettings(); }, ) ],