import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/kanji_item.dart'; import '../services/deck_repository.dart'; class BrowseScreen extends StatefulWidget { const BrowseScreen({super.key}); @override State createState() => _BrowseScreenState(); } class _BrowseScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; late PageController _kanjiPageController; late PageController _vocabPageController; List _kanjiDeck = []; List _vocabDeck = []; Map> _kanjiByLevel = {}; Map> _vocabByLevel = {}; List _kanjiSortedLevels = []; List _vocabSortedLevels = []; bool _loading = true; String _status = 'Loading...'; int _currentKanjiPage = 0; int _currentVocabPage = 0; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _kanjiPageController = PageController(); _vocabPageController = PageController(); _tabController.addListener(() { setState(() {}); // Rebuild to update the level selector }); _kanjiPageController.addListener(() { if (_kanjiPageController.page?.round() != _currentKanjiPage) { setState(() { _currentKanjiPage = _kanjiPageController.page!.round(); }); } }); _vocabPageController.addListener(() { if (_vocabPageController.page?.round() != _currentVocabPage) { setState(() { _currentVocabPage = _vocabPageController.page!.round(); }); } }); _loadDecks(); } Future _loadDecks() async { setState(() => _loading = true); try { final repo = Provider.of(context, listen: false); await repo.loadApiKey(); final apiKey = repo.apiKey; if (apiKey == null || apiKey.isEmpty) { setState(() { _status = 'API key not set.'; _loading = false; }); return; } var kanji = await repo.loadKanji(); if (kanji.isEmpty || kanji.every((k) => k.level == 0)) { setState(() => _status = 'Fetching kanji from WaniKani...'); kanji = await repo.fetchAndCacheFromWk(apiKey); } var vocab = await repo.loadVocabulary(); if (vocab.isEmpty || vocab.every((v) => v.level == 0)) { setState(() => _status = 'Fetching vocabulary from WaniKani...'); vocab = await repo.fetchAndCacheVocabularyFromWk(apiKey); } _kanjiDeck = kanji; _vocabDeck = vocab; _groupItemsByLevel(); setState(() { _loading = false; _status = 'Loaded ${_kanjiDeck.length} kanji and ${_vocabDeck.length} vocabulary.'; }); } catch (e) { setState(() { _status = 'Error: $e'; _loading = false; }); } } void _groupItemsByLevel() { _kanjiByLevel = {}; for (final item in _kanjiDeck) { if (item.level > 0) { (_kanjiByLevel[item.level] ??= []).add(item); } } _kanjiSortedLevels = _kanjiByLevel.keys.toList()..sort(); _vocabByLevel = {}; for (final item in _vocabDeck) { if (item.level > 0) { (_vocabByLevel[item.level] ??= []).add(item); } } _vocabSortedLevels = _vocabByLevel.keys.toList()..sort(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF121212), appBar: AppBar( title: const Text('Browse Items'), backgroundColor: const Color(0xFF1F1F1F), foregroundColor: Colors.white, bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: 'Kanji'), Tab(text: 'Vocabulary'), ], labelColor: Colors.blueAccent, unselectedLabelColor: Colors.grey, indicatorColor: Colors.blueAccent, ), ), body: _loading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.blueAccent), const SizedBox(height: 16), Text(_status, style: const TextStyle(color: Colors.white)), ], ), ) : Column( children: [ Expanded( child: TabBarView( controller: _tabController, children: [ _buildPaginatedView( _kanjiByLevel, _kanjiSortedLevels, _kanjiPageController, (items) => _buildGridView(items.cast())), _buildPaginatedView( _vocabByLevel, _vocabSortedLevels, _vocabPageController, (items) => _buildListView(items.cast())), ], ), ), SafeArea( top: false, child: _buildLevelSelector(), ), ], ), ); } Widget _buildPaginatedView( Map> groupedItems, List sortedLevels, PageController pageController, Widget Function(List) buildPageContent) { if (sortedLevels.isEmpty) { return const Center( child: Text('No items to display.', style: TextStyle(color: Colors.white)), ); } return PageView.builder( controller: pageController, itemCount: sortedLevels.length, itemBuilder: (context, index) { final level = sortedLevels[index]; final levelItems = groupedItems[level]!; return Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Level $level', style: const TextStyle( fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold), ), ), Expanded(child: buildPageContent(levelItems)), ], ); }, ); } Widget _buildLevelSelector() { final isKanji = _tabController.index == 0; final levels = isKanji ? _kanjiSortedLevels : _vocabSortedLevels; final controller = isKanji ? _kanjiPageController : _vocabPageController; final currentPage = isKanji ? _currentKanjiPage : _currentVocabPage; if (levels.isEmpty) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(vertical: 8.0), color: const Color(0xFF1F1F1F), 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( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton( onPressed: () { controller.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? Colors.blueAccent : const Color(0xFF333333), foregroundColor: Colors.white, shape: const CircleBorder(), padding: const EdgeInsets.all(12), ), child: Text(level.toString()), ), ); }), ), ), ); } Widget _buildGridView(List items) { return GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150, childAspectRatio: 1, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return GestureDetector( onTap: () => _showReadingsDialog(item), child: _buildSrsItemCard(item.characters, item.srsItems.values.toList()), ); }, padding: const EdgeInsets.all(8), ); } Widget _buildListView(List items) { return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return _buildVocabListTile(item); }, ); } Widget _buildVocabListTile(VocabularyItem item) { final avgSrsStage = item.srsItems.isNotEmpty ? item.srsItems.values .map((s) => s.srsStage) .reduce((a, b) => a + b) / item.srsItems.length : 0.0; return Card( color: const Color(0xFF1E1E1E), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ Expanded( child: Text( item.characters, style: const TextStyle(fontSize: 24, color: Colors.white), ), ), const SizedBox(width: 16), Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.meanings.join(', '), style: const TextStyle(color: Colors.grey), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), _buildSrsIndicator(avgSrsStage.round()), ], ), ), ], ), ), ); } Widget _buildSrsItemCard(String characters, List srsItems) { final avgSrsStage = srsItems.isNotEmpty ? srsItems.map((s) => s.srsStage).reduce((a, b) => a + b) / srsItems.length : 0.0; return Card( color: const Color(0xFF1E1E1E), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( characters, style: const TextStyle(fontSize: 32, color: Colors.white), textAlign: TextAlign.center, ), const SizedBox(height: 8), _buildSrsIndicator(avgSrsStage.round()), ], ), ), ); } Widget _buildSrsIndicator(int level) { return Tooltip( message: 'SRS Level: $level', child: ClipRRect( borderRadius: BorderRadius.circular(4), child: SizedBox( height: 10, child: LinearProgressIndicator( value: level / 9.0, // Max SRS level is 9 backgroundColor: Colors.grey[800], valueColor: AlwaysStoppedAnimation( _getColorForSrsLevel(level), ), ), ), ), ); } Color _getColorForSrsLevel(int level) { if (level >= 9) return Colors.purple; if (level >= 8) return Colors.blue; if (level >= 7) return Colors.lightBlue; if (level >= 5) return Colors.green; if (level >= 3) return Colors.yellow; if (level >= 1) return Colors.orange; return Colors.red; } void _showReadingsDialog(KanjiItem kanji) { showDialog( context: context, builder: (context) { return AlertDialog( backgroundColor: const Color(0xFF1E1E1E), title: Text( 'Details for ${kanji.characters}', style: const TextStyle(color: Colors.white), ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Level: ${kanji.level}', style: const TextStyle(color: Colors.white), ), const SizedBox(height: 16), if (kanji.meanings.isNotEmpty) Text( 'Meanings: ${kanji.meanings.join(', ')}', style: const TextStyle(color: Colors.white), ), const SizedBox(height: 16), if (kanji.onyomi.isNotEmpty) Text( 'On\'yomi: ${kanji.onyomi.join(', ')}', style: const TextStyle(color: Colors.white), ), if (kanji.kunyomi.isNotEmpty) Text( 'Kun\'yomi: ${kanji.kunyomi.join(', ')}', style: const TextStyle(color: Colors.white), ), if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty) const Text( 'No readings available.', style: TextStyle(color: Colors.white), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Close', style: TextStyle(color: Colors.blueAccent)), ), ], ); }, ); } @override void dispose() { _tabController.dispose(); _kanjiPageController.dispose(); _vocabPageController.dispose(); super.dispose(); } }