add a shit ton of feature for the custom srs
This commit is contained in:
@@ -2,7 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/kanji_item.dart';
|
||||
import '../services/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';
|
||||
|
||||
class BrowseScreen extends StatefulWidget {
|
||||
const BrowseScreen({super.key});
|
||||
@@ -18,11 +21,17 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
|
||||
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;
|
||||
@@ -32,7 +41,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_kanjiPageController = PageController();
|
||||
_vocabPageController = PageController();
|
||||
|
||||
@@ -57,6 +66,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
});
|
||||
|
||||
_loadDecks();
|
||||
_loadCustomDeck();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -67,144 +77,58 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadDecks() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final repo = Provider.of<DeckRepository>(context, listen: false);
|
||||
await repo.loadApiKey();
|
||||
final apiKey = repo.apiKey;
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty) {
|
||||
setState(() {
|
||||
_apiKeyMissing = true;
|
||||
_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.';
|
||||
_apiKeyMissing = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_status = 'Error: $e';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
Future<void> _loadCustomDeck() async {
|
||||
final customDeck = await _customDeckRepository.getCustomDeck();
|
||||
setState(() {
|
||||
_customDeck = customDeck;
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
Widget _buildWaniKaniTab(Widget child) {
|
||||
if (_apiKeyMissing) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Browse')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('WaniKani API key is not set.', style: TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
_loadDecks();
|
||||
},
|
||||
child: const Text('Go to Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('WaniKani API key is not set.', style: TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
_loadDecks();
|
||||
},
|
||||
child: const Text('Go to Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Browse'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: 'Kanji'),
|
||||
Tab(text: 'Vocabulary'),
|
||||
if (_loading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(color: Colors.blueAccent),
|
||||
const SizedBox(height: 16),
|
||||
Text(_status, style: const TextStyle(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0xFF121212),
|
||||
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<KanjiItem>())),
|
||||
_buildPaginatedView(
|
||||
_vocabByLevel,
|
||||
_vocabSortedLevels,
|
||||
_vocabPageController,
|
||||
(items) => _buildListView(items.cast<VocabularyItem>())),
|
||||
],
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: _buildLevelSelector(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildCustomSrsTab() {
|
||||
if (_customDeck.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No custom cards yet.', style: TextStyle(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
return _buildCustomGridView(_customDeck);
|
||||
}
|
||||
|
||||
Widget _buildPaginatedView(
|
||||
@@ -452,7 +376,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty)
|
||||
const Text(
|
||||
'No readings available.',
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -467,4 +391,290 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadDecks() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final repo = Provider.of<DeckRepository>(context, listen: false);
|
||||
await repo.loadApiKey();
|
||||
final apiKey = repo.apiKey;
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty) {
|
||||
setState(() {
|
||||
_apiKeyMissing = true;
|
||||
_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.';
|
||||
_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();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: _isSelectionMode ? _buildSelectionAppBar() : _buildDefaultAppBar(),
|
||||
backgroundColor: const Color(0xFF121212),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildWaniKaniTab(
|
||||
_buildPaginatedView(
|
||||
_kanjiByLevel,
|
||||
_kanjiSortedLevels,
|
||||
_kanjiPageController,
|
||||
(items) => _buildGridView(items.cast<KanjiItem>())),
|
||||
),
|
||||
_buildWaniKaniTab(
|
||||
_buildPaginatedView(
|
||||
_vocabByLevel,
|
||||
_vocabSortedLevels,
|
||||
_vocabPageController,
|
||||
(items) => _buildListView(items.cast<VocabularyItem>())),
|
||||
),
|
||||
_buildCustomSrsTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!_isSelectionMode)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: _tabController.index < 2 ? _buildLevelSelector() : const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: const Icon(Icons.timer_off),
|
||||
onPressed: _toggleIntervalForSelected,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _selectAll() {
|
||||
setState(() {
|
||||
if (_selectedItems.length == _customDeck.length) {
|
||||
_selectedItems.clear();
|
||||
} else {
|
||||
_selectedItems = List.from(_customDeck);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _deleteSelected() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Selected'),
|
||||
content: Text('Are you sure you want to delete ${_selectedItems.length} cards?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
for (final item in _selectedItems) {
|
||||
await _customDeckRepository.deleteCard(item);
|
||||
}
|
||||
setState(() {
|
||||
_isSelectionMode = false;
|
||||
_selectedItems.clear();
|
||||
});
|
||||
_loadCustomDeck();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleIntervalForSelected() {
|
||||
for (final item in _selectedItems) {
|
||||
final updatedItem = CustomKanjiItem(
|
||||
characters: item.characters,
|
||||
meaning: item.meaning,
|
||||
kanji: item.kanji,
|
||||
useInterval: !item.useInterval,
|
||||
srsLevel: item.srsLevel,
|
||||
nextReview: item.nextReview,
|
||||
);
|
||||
_customDeckRepository.updateCard(updatedItem);
|
||||
}
|
||||
setState(() {
|
||||
_isSelectionMode = false;
|
||||
_selectedItems.clear();
|
||||
});
|
||||
_loadCustomDeck();
|
||||
}
|
||||
|
||||
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(
|
||||
color: isSelected
|
||||
? Colors.blue.withOpacity(0.5)
|
||||
: item.useInterval
|
||||
? Color.lerp(const Color(0xFF1E1E1E), Colors.blue, 0.1)
|
||||
: const Color(0xFF1E1E1E),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
item.kanji ?? item.characters,
|
||||
style: const TextStyle(fontSize: 32, color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item.meaning,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildSrsIndicator(item.srsLevel),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
padding: const EdgeInsets.all(8),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user