import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/kanji_item.dart'; import '../services/deck_repository.dart'; import '../services/distractor_generator.dart'; import '../widgets/kanji_card.dart'; import '../widgets/options_grid.dart'; import 'package:audioplayers/audioplayers.dart'; import 'settings_screen.dart'; class _ReadingInfo { final List correctReadings; final String hint; _ReadingInfo(this.correctReadings, this.hint); } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { List _deck = []; bool _loading = false; String _status = 'Loading deck...'; final DistractorGenerator _dg = DistractorGenerator(); final Random _random = Random(); final _audioPlayer = AudioPlayer(); QuizMode _mode = QuizMode.kanjiToEnglish; KanjiItem? _current; List _options = []; List _correctAnswers = []; String _readingHint = ''; int _score = 0; int _asked = 0; @override void initState() { super.initState(); _loadDeck(); } Future _loadDeck() async { setState(() { _loading = true; _status = 'Loading deck...'; }); try { final repo = Provider.of(context, listen: false); await repo.loadApiKey(); final apiKey = repo.apiKey; if (apiKey == null || apiKey.isEmpty) { if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); } return; } var items = await repo.loadKanji(); if (items.isEmpty) { setState(() { _status = 'Fetching deck...'; }); items = await repo.fetchAndCacheFromWk(apiKey); } setState(() { _deck = items; _status = 'Loaded ${items.length} kanji'; _loading = false; }); _nextQuestion(); } catch (e) { setState(() { _status = 'Error: $e'; _loading = false; }); } } String _toTitleCase(String s) { if (s.isEmpty) return s; return s .split(' ') .map((w) => w.isEmpty ? w : w[0].toUpperCase() + w.substring(1)) .join(' '); } _ReadingInfo _pickReading(KanjiItem item) { final choices = []; if (item.onyomi.isNotEmpty) choices.add('onyomi'); if (item.kunyomi.isNotEmpty) choices.add('kunyomi'); if (choices.isEmpty) return _ReadingInfo([], ''); final pickedType = choices[_random.nextInt(choices.length)]; final readingsList = pickedType == 'onyomi' ? item.onyomi : item.kunyomi; final hint = 'Select the ${pickedType == 'onyomi' ? "on'yomi" : "kunyomi"}'; return _ReadingInfo(readingsList, hint); } void _nextQuestion() { _deck.sort((a, b) { final aSrsItem = a.srsItems[_mode.toString()]; final bSrsItem = b.srsItems[_mode.toString()]; final aStage = aSrsItem?.srsStage ?? 0; final bStage = bSrsItem?.srsStage ?? 0; if (aStage != bStage) { return aStage.compareTo(bStage); } final aLastAsked = aSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); final bLastAsked = bSrsItem?.lastAsked ?? DateTime.fromMillisecondsSinceEpoch(0); return aLastAsked.compareTo(bLastAsked); }); _current = _deck.first; _correctAnswers = []; _options = []; _readingHint = ''; switch (_mode) { case QuizMode.kanjiToEnglish: _correctAnswers = [_current!.meanings.first]; _options = [ _correctAnswers.first, ..._dg.generateMeanings(_current!, _deck, 3) ].map(_toTitleCase).toList() ..shuffle(); break; case QuizMode.englishToKanji: _correctAnswers = [_current!.characters]; _options = [ _correctAnswers.first, ..._dg.generateKanji(_current!, _deck, 3) ]..shuffle(); break; case QuizMode.reading: final info = _pickReading(_current!); _correctAnswers = info.correctReadings; _readingHint = info.hint; final readingsSource = _readingHint.contains("on'yomi") ? _deck.expand((k) => k.onyomi) : _deck.expand((k) => k.kunyomi); final distractors = readingsSource .where((r) => !_correctAnswers.contains(r)) .toSet() .toList() ..shuffle(); _options = ([ _correctAnswers[_random.nextInt(_correctAnswers.length)], ...distractors.take(3) ]) ..shuffle(); break; } setState(() {}); } void _answer(String option) async { final isCorrect = _correctAnswers .map((a) => a.toLowerCase().trim()) .contains(option.toLowerCase().trim()); final repo = Provider.of(context, listen: false); final current = _current!; String readingType = ''; if (_mode == QuizMode.reading) { readingType = _readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi'; } final srsKey = _mode.toString() + readingType; var srsItem = current.srsItems[srsKey]; final isNew = srsItem == null; final srsItemForUpdate = srsItem ??= SrsItem(kanjiId: current.id, quizMode: _mode, readingType: readingType); setState(() { _asked += 1; if (isCorrect) { _score += 1; _audioPlayer.play(AssetSource('sfx/confirm.mp3')); } else { srsItemForUpdate.srsStage = max(0, srsItemForUpdate.srsStage - 1); } srsItemForUpdate.lastAsked = DateTime.now(); current.srsItems[srsKey] = srsItemForUpdate; }); if (isNew) { await repo.insertSrsItem(srsItemForUpdate); } else { await repo.updateSrsItem(srsItemForUpdate); } final correctDisplay = (_mode == QuizMode.kanjiToEnglish) ? _toTitleCase(_correctAnswers.first) : (_mode == QuizMode.reading ? _correctAnswers.join(', ') : _correctAnswers.first); final snack = SnackBar( content: Text( isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay', style: TextStyle( color: isCorrect ? Colors.greenAccent : Colors.redAccent, fontWeight: FontWeight.bold, ), ), backgroundColor: const Color(0xFF222222), duration: const Duration(milliseconds: 900), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(snack); } Future.delayed(const Duration(milliseconds: 900), _nextQuestion); } @override Widget build(BuildContext context) { String prompt = ''; String subtitle = ''; switch (_mode) { case QuizMode.kanjiToEnglish: prompt = _current?.characters ?? ''; break; case QuizMode.englishToKanji: prompt = _current != null ? _toTitleCase(_current!.meanings.first) : ''; break; case QuizMode.reading: prompt = _current?.characters ?? ''; subtitle = _readingHint; break; } return Scaffold( backgroundColor: const Color(0xFF121212), appBar: AppBar( title: const Text('WaniKani Kanji SRS'), backgroundColor: const Color(0xFF1F1F1F), foregroundColor: Colors.white, elevation: 2, actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); }, ) ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( children: [ Expanded( child: Text( _status, style: const TextStyle(color: Colors.white), ), ), if (_loading) const CircularProgressIndicator(color: Colors.blueAccent), ], ), const SizedBox(height: 12), Wrap( spacing: 6, runSpacing: 4, alignment: WrapAlignment.center, children: [ _buildChoiceChip('Kanji→English', QuizMode.kanjiToEnglish), _buildChoiceChip('English→Kanji', QuizMode.englishToKanji), _buildChoiceChip('Reading', QuizMode.reading), ], ), const SizedBox(height: 18), Expanded( flex: 3, child: Center( child: ConstrainedBox( constraints: const BoxConstraints( minWidth: 0, maxWidth: 500, minHeight: 150, ), child: KanjiCard( characters: prompt, subtitle: subtitle, backgroundColor: const Color(0xFF1E1E1E), textColor: Colors.white, ), ), ), ), const SizedBox(height: 12), SafeArea( top: false, child: Column( children: [ OptionsGrid( options: _options, onSelected: _answer, buttonColor: const Color(0xFF1E1E1E), textColor: Colors.white, ), const SizedBox(height: 8), Text( 'Score: $_score / $_asked', style: const TextStyle(color: Colors.white), ), ], ), ), ], ), ), ); } ChoiceChip _buildChoiceChip(String label, QuizMode mode) { final selected = _mode == mode; return ChoiceChip( label: Text( label, style: TextStyle(color: selected ? Colors.white : Colors.grey[400]), ), selected: selected, onSelected: (v) { setState(() => _mode = mode); _nextQuestion(); }, selectedColor: Colors.blueAccent, backgroundColor: const Color(0xFF1E1E1E), ); } }