Files
Hirameki-SRS/lib/src/screens/browse_screen.dart

1186 lines
36 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hirameki_srs/src/themes.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import '../models/kanji_item.dart';
import '../models/vocabulary_item.dart';
import '../models/srs_item.dart';
import '../services/deck_repository.dart';
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
import '../services/custom_deck_repository.dart';
import '../models/custom_kanji_item.dart';
import 'settings_screen.dart';
import 'custom_card_details_screen.dart';
import 'add_card_screen.dart';
class BrowseScreen extends StatefulWidget {
const BrowseScreen({super.key});
@override
State<BrowseScreen> createState() => _BrowseScreenState();
}
class _BrowseScreenState extends State<BrowseScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
late PageController _kanjiPageController;
late PageController _vocabPageController;
List<KanjiItem> _kanjiDeck = [];
List<VocabularyItem> _vocabDeck = [];
List<CustomKanjiItem> _customDeck = [];
Map<int, List<KanjiItem>> _kanjiByLevel = {};
Map<int, List<VocabularyItem>> _vocabByLevel = {};
List<int> _kanjiSortedLevels = [];
List<int> _vocabSortedLevels = [];
final _customDeckRepository = CustomDeckRepository();
bool _isSelectionMode = false;
List<CustomKanjiItem> _selectedItems = [];
bool _loading = true;
String _status = 'Loading...';
int _currentKanjiPage = 0;
int _currentVocabPage = 0;
bool _apiKeyMissing = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_kanjiPageController = PageController();
_vocabPageController = PageController();
_tabController.addListener(() {
setState(() {});
});
_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();
_loadCustomDeck();
}
@override
void dispose() {
_tabController.dispose();
_kanjiPageController.dispose();
_vocabPageController.dispose();
super.dispose();
}
Future<void> _loadCustomDeck() async {
final customDeck = await _customDeckRepository.getCustomDeck();
setState(() {
_customDeck = customDeck;
});
}
Widget _buildWaniKaniTab(Widget child) {
if (_apiKeyMissing) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'WaniKani API key is not set.',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
if (!mounted) return;
_loadDecks();
},
child: const Text('Go to Settings'),
),
],
),
);
}
if (_loading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
_status,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
],
),
);
}
return child;
}
Widget _buildCustomSrsTab() {
if (_customDeck.isEmpty) {
return Center(
child: Text(
'No custom cards yet.',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
return _buildCustomGridView(_customDeck);
}
Widget _buildPaginatedView(
Map<int, List<dynamic>> groupedItems,
List<int> sortedLevels,
PageController pageController,
Widget Function(List<dynamic>) buildPageContent,
dynamic repository,
) {
if (sortedLevels.isEmpty) {
return Center(
child: Text(
'No items to display.',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
return PageView.builder(
controller: pageController,
itemCount: sortedLevels.length,
itemBuilder: (context, index) {
final level = sortedLevels[index];
final levelItems = groupedItems[level]!;
final bool isDisabled = levelItems.every(
(item) => (item as dynamic).srsItems.values.every(
(srs) => (srs as SrsItem).disabled,
),
);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Level $level',
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Checkbox(
value: !isDisabled,
onChanged: (value) {
_toggleLevelExclusion(level, repository);
},
),
],
),
),
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: Theme.of(context).colorScheme.surfaceContainer,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(levels.length, (index) {
final level = levels[index];
final isSelected = index == currentPage;
final items = isKanji ? _kanjiByLevel[level] : _vocabByLevel[level];
final bool isDisabled =
items?.every(
(item) => (item as dynamic).srsItems.values.every(
(srs) => (srs as SrsItem).disabled,
),
) ??
false;
return Expanded(
child: 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
? Theme.of(context).colorScheme.primary
: isDisabled
? Theme.of(context).colorScheme.surfaceVariant
: Theme.of(context).colorScheme.surfaceContainerHighest,
foregroundColor: isSelected
? Theme.of(context).colorScheme.onPrimary
: isDisabled
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
),
child: Text(level.toString()),
),
),
);
}),
),
);
}
Widget _buildGridView(List<KanjiItem> 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),
);
},
padding: const EdgeInsets.all(8),
);
}
Widget _buildListView(List<VocabularyItem> items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return _buildVocabListTile(item);
},
);
}
Widget _buildVocabListTile(VocabularyItem item) {
final requiredModes = <String>[
QuizMode.vocabToEnglish.toString(),
QuizMode.englishToVocab.toString(),
QuizMode.audioToEnglish.toString(),
];
int minSrsStage = 9;
for (final mode in requiredModes) {
final srsItem = item.srsItems[mode];
if (srsItem == null) {
minSrsStage = 0;
break;
}
if (srsItem.srsStage < minSrsStage) {
minSrsStage = srsItem.srsStage;
}
}
return GestureDetector(
onTap: () => _showVocabDetailsDialog(context, item),
child: Card(
color: Theme.of(context).colorScheme.surfaceContainer,
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: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.meanings.join(', '),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
_buildSrsIndicator(minSrsStage),
],
),
),
],
),
),
),
);
}
Widget _buildSrsItemCard(KanjiItem item) {
final requiredModes = <String>[
QuizMode.kanjiToEnglish.toString(),
QuizMode.englishToKanji.toString(),
];
if (item.onyomi.isNotEmpty) {
requiredModes.add('${QuizMode.reading}onyomi');
}
if (item.kunyomi.isNotEmpty) {
requiredModes.add('${QuizMode.reading}kunyomi');
}
int minSrsStage = 9;
for (final mode in requiredModes) {
final srsItem = item.srsItems[mode];
if (srsItem == null) {
minSrsStage = 0;
break;
}
if (srsItem.srsStage < minSrsStage) {
minSrsStage = srsItem.srsStage;
}
}
return Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.characters,
style: TextStyle(
fontSize: 32,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
_buildSrsIndicator(minSrsStage),
],
),
),
);
}
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,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
_getColorForSrsLevel(level),
),
),
),
),
);
}
Color _getColorForSrsLevel(int level) {
final srsColors = Theme.of(context).srsColors;
if (level >= 9) return srsColors.level9;
if (level >= 8) return srsColors.level8;
if (level >= 7) return srsColors.level7;
if (level >= 6) return srsColors.level6;
if (level >= 5) return srsColors.level5;
if (level >= 4) return srsColors.level4;
if (level >= 3) return srsColors.level3;
if (level >= 2) return srsColors.level2;
if (level >= 1) return srsColors.level1;
return Colors.grey;
}
void _showReadingsDialog(KanjiItem kanji) {
final srsScores = <String, int>{
'JP -> EN': 0,
'EN -> JP': 0,
'Reading (onyomi)': 0,
'Reading (kunyomi)': 0,
};
for (final entry in kanji.srsItems.entries) {
final srsItem = entry.value;
switch (srsItem.quizMode) {
case QuizMode.kanjiToEnglish:
srsScores['JP -> EN'] = srsItem.srsStage;
break;
case QuizMode.englishToKanji:
srsScores['EN -> JP'] = srsItem.srsStage;
break;
case QuizMode.reading:
if (srsItem.readingType == 'onyomi') {
srsScores['Reading (onyomi)'] = srsItem.srsStage;
} else if (srsItem.readingType == 'kunyomi') {
srsScores['Reading (kunyomi)'] = srsItem.srsStage;
}
break;
default:
break;
}
}
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text(
'Details for ${kanji.characters}',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Level: ${kanji.level}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
if (kanji.meanings.isNotEmpty)
Text(
'Meanings: ${kanji.meanings.join(', ')}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
if (kanji.onyomi.isNotEmpty)
Text(
'On\'yomi: ${kanji.onyomi.join(', ')}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
if (kanji.kunyomi.isNotEmpty)
Text(
'Kun\'yomi: ${kanji.kunyomi.join(', ')}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty)
Text(
'No readings available.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 16),
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(height: 16),
Text(
'SRS Scores:',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
...srsScores.entries.map(
(entry) => Text(
' ${entry.key}: ${entry.value}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(
'Close',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
],
);
},
);
}
Future<void> _loadDecks() async {
setState(() => _loading = true);
try {
final kanjiRepo = Provider.of<DeckRepository>(context, listen: false);
final vocabRepo = Provider.of<VocabDeckRepository>(
context,
listen: false,
);
await kanjiRepo.loadApiKey();
final apiKey = kanjiRepo.apiKey;
if (apiKey == null || apiKey.isEmpty) {
setState(() {
_apiKeyMissing = true;
_loading = false;
});
return;
}
var kanji = await kanjiRepo.loadKanji();
if (kanji.isEmpty || kanji.every((k) => k.level == 0)) {
setState(() => _status = 'Fetching kanji from WaniKani...');
kanji = await kanjiRepo.fetchAndCacheFromWk(apiKey);
}
var vocab = await vocabRepo.loadVocabulary();
if (vocab.isEmpty || vocab.every((v) => v.level == 0)) {
setState(() => _status = 'Fetching vocabulary from WaniKani...');
vocab = await vocabRepo.fetchAndCacheVocabularyFromWk(apiKey);
}
_kanjiDeck = kanji;
_vocabDeck = vocab;
_groupItemsByLevel();
setState(() {
_loading = false;
_status =
'Loaded ${_kanjiDeck.length} kanji and ${_vocabDeck.length} vocabulary.';
_apiKeyMissing = false;
});
} 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();
}
Future<void> _toggleLevelExclusion(int level, dynamic repository) async {
final List<SrsItem> itemsToUpdate = [];
final bool currentlyDisabled;
if (repository is DeckRepository) {
final items = _kanjiByLevel[level] ?? [];
currentlyDisabled = items.every(
(item) => item.srsItems.values.every((srs) => srs.disabled),
);
for (final item in items) {
for (final srsItem in item.srsItems.values) {
itemsToUpdate.add(srsItem);
}
}
} else if (repository is VocabDeckRepository) {
final items = _vocabByLevel[level] ?? [];
currentlyDisabled = items.every(
(item) => item.srsItems.values.every((srs) => srs.disabled),
);
for (final item in items) {
for (final srsItem in item.srsItems.values) {
itemsToUpdate.add(srsItem);
}
}
} else {
return;
}
for (final item in itemsToUpdate) {
item.disabled = !currentlyDisabled;
}
await repository.updateSrsItems(itemsToUpdate);
_loadDecks();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _isSelectionMode
? _buildSelectionAppBar()
: _buildDefaultAppBar(),
body: Column(
children: [
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildWaniKaniTab(
_buildPaginatedView(
_kanjiByLevel,
_kanjiSortedLevels,
_kanjiPageController,
(items) => _buildGridView(items.cast<KanjiItem>()),
Provider.of<DeckRepository>(context, listen: false),
),
),
_buildWaniKaniTab(
_buildPaginatedView(
_vocabByLevel,
_vocabSortedLevels,
_vocabPageController,
(items) => _buildListView(items.cast<VocabularyItem>()),
Provider.of<VocabDeckRepository>(context, listen: false),
),
),
_buildCustomSrsTab(),
],
),
),
if (!_isSelectionMode)
SafeArea(
top: false,
child: _tabController.index < 2
? _buildLevelSelector()
: const SizedBox.shrink(),
),
],
),
floatingActionButton: _tabController.index == 2
? FloatingActionButton(
onPressed: () async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => AddCardScreen()));
if (!mounted) return;
_loadCustomDeck();
},
child: const Icon(Icons.add),
)
: null,
);
}
AppBar _buildDefaultAppBar() {
return AppBar(
title: const Text('Browse'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Kanji'),
Tab(text: 'Vocabulary'),
Tab(text: 'Custom SRS'),
],
),
);
}
AppBar _buildSelectionAppBar() {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
_isSelectionMode = false;
_selectedItems.clear();
});
},
),
title: Text('${_selectedItems.length} selected'),
actions: [
IconButton(icon: const Icon(Icons.select_all), onPressed: _selectAll),
IconButton(icon: const Icon(Icons.delete), onPressed: _deleteSelected),
IconButton(
icon: Icon(_toggleIntervalIcon),
onPressed: _toggleIntervalForSelected,
),
],
);
}
IconData get _toggleIntervalIcon {
if (_selectedItems.isEmpty) {
return Icons.timer_off;
}
final bool willEnable = _selectedItems.any((item) => !item.useInterval);
return willEnable ? Icons.timer : Icons.timer_off;
}
void _selectAll() {
setState(() {
if (_selectedItems.length == _customDeck.length) {
_selectedItems.clear();
} else {
_selectedItems = List.from(_customDeck);
}
});
}
void _deleteSelected() {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Delete Selected'),
content: Text(
'Are you sure you want to delete ${_selectedItems.length} cards?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
() async {
for (final item in _selectedItems) {
await _customDeckRepository.deleteCard(item);
}
if (!mounted) return;
setState(() {
_isSelectionMode = false;
_selectedItems.clear();
});
_loadCustomDeck();
}();
},
child: const Text('Delete'),
),
],
),
);
}
Future<void> _toggleIntervalForSelected() async {
if (_selectedItems.isEmpty) {
return;
}
final bool targetState = _selectedItems.any((item) => !item.useInterval);
final selectedCharacters = _selectedItems
.map((item) => item.characters)
.toSet();
final List<CustomKanjiItem> updatedItems = [];
for (final item in _selectedItems) {
final updatedItem = CustomKanjiItem(
characters: item.characters,
meaning: item.meaning,
kanji: item.kanji,
useInterval: targetState,
srsData: item.srsData,
);
updatedItems.add(updatedItem);
}
await _customDeckRepository.updateCards(updatedItems);
await _loadCustomDeck();
final newSelectedItems = _customDeck
.where((item) => selectedCharacters.contains(item.characters))
.toList();
setState(() {
_selectedItems = newSelectedItems;
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
});
}
Widget _buildCustomGridView(List<CustomKanjiItem> items) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 1.2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
final isSelected = _selectedItems.contains(item);
return GestureDetector(
onLongPress: () {
setState(() {
_isSelectionMode = true;
_selectedItems.add(item);
});
},
onTap: () {
if (_isSelectionMode) {
setState(() {
if (isSelected) {
_selectedItems.remove(item);
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
} else {
_selectedItems.add(item);
}
});
} else {
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (_) => CustomCardDetailsScreen(
item: item,
repository: _customDeckRepository,
),
),
)
.then((_) => _loadCustomDeck());
}
},
child: Card(
shape: RoundedRectangleBorder(
side: isSelected
? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.0,
)
: BorderSide.none,
borderRadius: BorderRadius.circular(12.0),
),
color: isSelected
? Theme.of(
context,
).colorScheme.primary.withAlpha((255 * 0.5).round())
: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
item.kanji?.isNotEmpty == true
? item.kanji!
: item.characters,
style: TextStyle(
fontSize: 32,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
Text(
item.meaning,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 16,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Builder(
builder: (context) {
final avgSrs =
(item.srsData.japaneseToEnglish +
item.srsData.englishToJapanese +
item.srsData.listeningComprehension) /
3;
return _buildSrsIndicator(avgSrs.round());
},
),
],
),
),
if (item.useInterval)
Positioned(
top: 4,
right: 4,
child: Icon(
Icons.timer,
color: Theme.of(context).colorScheme.tertiary,
size: 16,
),
),
],
),
),
);
},
padding: const EdgeInsets.all(8),
);
}
}
class _VocabDetailsDialog extends StatefulWidget {
final VocabularyItem vocab;
final ThemeData theme;
const _VocabDetailsDialog({required this.vocab, required this.theme});
@override
State<_VocabDetailsDialog> createState() => _VocabDetailsDialogState();
}
class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
List<Widget> _exampleSentences = [const CircularProgressIndicator()];
@override
void initState() {
super.initState();
_fetchExampleSentences();
}
Future<void> _fetchExampleSentences() async {
try {
final uri = Uri.parse(
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}',
);
final response = await http.get(uri);
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
final sentences = <Widget>[];
if (data['data'] != null && (data['data'] as List).isNotEmpty) {
for (final result in data['data']) {
if (result['japanese'] != null &&
(result['japanese'] as List).isNotEmpty &&
result['senses'] != null &&
(result['senses'] as List).isNotEmpty) {
final japaneseWord =
result['japanese'][0]['word'] ??
result['japanese'][0]['reading'];
final englishDefinition =
result['senses'][0]['english_definitions'].join(', ');
if (japaneseWord != null && englishDefinition != null) {
sentences.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
japaneseWord,
style: TextStyle(
color: widget.theme.colorScheme.onSurface,
),
),
Text(
englishDefinition,
style: TextStyle(
color: widget.theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
],
),
);
}
}
}
}
if (sentences.isEmpty) {
sentences.add(
Text(
'No example sentences found.',
style: TextStyle(color: widget.theme.colorScheme.onSurface),
),
);
}
if (mounted) {
setState(() {
_exampleSentences = sentences;
});
}
} else {
if (mounted) {
setState(() {
_exampleSentences = [
Text(
'Failed to load example sentences.',
style: TextStyle(color: widget.theme.colorScheme.error),
),
];
});
}
}
} catch (e) {
if (mounted) {
setState(() {
_exampleSentences = [
Text(
'Error loading example sentences.',
style: TextStyle(color: widget.theme.colorScheme.error),
),
];
});
}
}
}
@override
Widget build(BuildContext context) {
final srsScores = <String, int>{'JP -> EN': 0, 'EN -> JP': 0, 'Audio': 0};
for (final entry in widget.vocab.srsItems.entries) {
final srsItem = entry.value;
switch (srsItem.quizMode) {
case QuizMode.vocabToEnglish:
srsScores['JP -> EN'] = srsItem.srsStage;
break;
case QuizMode.englishToVocab:
srsScores['EN -> JP'] = srsItem.srsStage;
break;
case QuizMode.audioToEnglish:
srsScores['Audio'] = srsItem.srsStage;
break;
default:
break;
}
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Level: ${widget.vocab.level}',
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: widget.theme.colorScheme.onSurface),
),
const SizedBox(height: 16),
if (widget.vocab.readings.isNotEmpty)
Text(
'Readings: ${widget.vocab.readings.join(', ')}',
style: TextStyle(color: widget.theme.colorScheme.onSurface),
),
const SizedBox(height: 16),
Divider(color: widget.theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 16),
Text(
'SRS Scores:',
style: TextStyle(
color: widget.theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
...srsScores.entries.map(
(entry) => Text(
' ${entry.key}: ${entry.value}',
style: TextStyle(color: widget.theme.colorScheme.onSurface),
),
),
const SizedBox(height: 16),
Divider(color: widget.theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 16),
Text(
'Example Sentences:',
style: TextStyle(
color: widget.theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
..._exampleSentences,
],
),
);
}
}
void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
final currentTheme = Theme.of(context);
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: currentTheme.colorScheme.surfaceContainer,
title: Text(
'Details for ${vocab.characters}',
style: TextStyle(color: currentTheme.colorScheme.onSurface),
),
content: _VocabDetailsDialog(vocab: vocab, theme: currentTheme),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(
'Close',
style: TextStyle(color: currentTheme.colorScheme.primary),
),
),
],
);
},
);
}