custom_srs #5
3
.gitignore
vendored
3
.gitignore
vendored
@@ -46,3 +46,6 @@ app.*.map.json
|
||||
|
||||
*.jks
|
||||
gradle.properties
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'src/services/deck_repository.dart';
|
||||
import 'src/screens/start_screen.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await dotenv.load(fileName: ".env");
|
||||
|
||||
runApp(
|
||||
Provider<DeckRepository>(
|
||||
|
||||
@@ -3,11 +3,17 @@ class CustomKanjiItem {
|
||||
final String characters;
|
||||
final String meaning;
|
||||
final String? kanji;
|
||||
final bool useInterval;
|
||||
int srsLevel;
|
||||
DateTime? nextReview;
|
||||
|
||||
CustomKanjiItem({
|
||||
required this.characters,
|
||||
required this.meaning,
|
||||
this.kanji,
|
||||
this.useInterval = false,
|
||||
this.srsLevel = 0,
|
||||
this.nextReview,
|
||||
});
|
||||
|
||||
factory CustomKanjiItem.fromJson(Map<String, dynamic> json) {
|
||||
@@ -15,6 +21,11 @@ class CustomKanjiItem {
|
||||
characters: json['characters'] as String,
|
||||
meaning: json['meaning'] as String,
|
||||
kanji: json['kanji'] as String?,
|
||||
useInterval: json['useInterval'] as bool? ?? false,
|
||||
srsLevel: json['srsLevel'] as int? ?? 0,
|
||||
nextReview: json['nextReview'] != null
|
||||
? DateTime.parse(json['nextReview'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +34,9 @@ class CustomKanjiItem {
|
||||
'characters': characters,
|
||||
'meaning': meaning,
|
||||
'kanji': kanji,
|
||||
'useInterval': useInterval,
|
||||
'srsLevel': srsLevel,
|
||||
'nextReview': nextReview?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
final _kanjiController = TextEditingController();
|
||||
final _kanaKit = const KanaKit();
|
||||
final _deckRepository = CustomDeckRepository();
|
||||
bool _useInterval = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,6 +54,8 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
characters: _japaneseController.text,
|
||||
meaning: _englishController.text,
|
||||
kanji: _kanjiController.text.isNotEmpty ? _kanjiController.text : null,
|
||||
useInterval: _useInterval,
|
||||
nextReview: _useInterval ? DateTime.now() : null,
|
||||
);
|
||||
_deckRepository.addCard(newItem);
|
||||
Navigator.of(context).pop();
|
||||
@@ -106,6 +109,16 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Use Interval-based SRS'),
|
||||
value: _useInterval,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useInterval = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: _saveCard,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
127
lib/src/screens/custom_card_details_screen.dart
Normal file
127
lib/src/screens/custom_card_details_screen.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/custom_kanji_item.dart';
|
||||
import '../services/custom_deck_repository.dart';
|
||||
|
||||
class CustomCardDetailsScreen extends StatefulWidget {
|
||||
final CustomKanjiItem item;
|
||||
final CustomDeckRepository repository;
|
||||
|
||||
const CustomCardDetailsScreen(
|
||||
{super.key, required this.item, required this.repository});
|
||||
|
||||
@override
|
||||
State<CustomCardDetailsScreen> createState() =>
|
||||
_CustomCardDetailsScreenState();
|
||||
}
|
||||
|
||||
class _CustomCardDetailsScreenState extends State<CustomCardDetailsScreen> {
|
||||
late TextEditingController _japaneseController;
|
||||
late TextEditingController _englishController;
|
||||
late TextEditingController _kanjiController;
|
||||
late bool _useInterval;
|
||||
late int _srsLevel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_japaneseController = TextEditingController(text: widget.item.characters);
|
||||
_englishController = TextEditingController(text: widget.item.meaning);
|
||||
_kanjiController = TextEditingController(text: widget.item.kanji);
|
||||
_useInterval = widget.item.useInterval;
|
||||
_srsLevel = widget.item.srsLevel;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_japaneseController.dispose();
|
||||
_englishController.dispose();
|
||||
_kanjiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _saveChanges() {
|
||||
final updatedItem = CustomKanjiItem(
|
||||
characters: _japaneseController.text,
|
||||
meaning: _englishController.text,
|
||||
kanji: _kanjiController.text,
|
||||
useInterval: _useInterval,
|
||||
srsLevel: _srsLevel,
|
||||
nextReview: widget.item.nextReview,
|
||||
);
|
||||
widget.repository.updateCard(updatedItem);
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
void _deleteCard() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Card'),
|
||||
content: const Text('Are you sure you want to delete this card?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.repository.deleteCard(widget.item);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Edit Card'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: _deleteCard,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _japaneseController,
|
||||
decoration: const InputDecoration(labelText: 'Japanese (Kana)'),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _kanjiController,
|
||||
decoration: const InputDecoration(labelText: 'Japanese (Kanji)'),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _englishController,
|
||||
decoration: const InputDecoration(labelText: 'English'),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Use Interval SRS'),
|
||||
value: _useInterval,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useInterval = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('SRS Level: $_srsLevel'),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _saveChanges,
|
||||
child: const Text('Save Changes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,23 @@ enum CustomQuizMode { japaneseToEnglish, englishToJapanese, listeningComprehensi
|
||||
class CustomQuizScreen extends StatefulWidget {
|
||||
final List<CustomKanjiItem> deck;
|
||||
final CustomQuizMode quizMode;
|
||||
final Function(CustomKanjiItem) onCardReviewed;
|
||||
final bool useKanji;
|
||||
|
||||
const CustomQuizScreen(
|
||||
{super.key, required this.deck, required this.quizMode});
|
||||
const CustomQuizScreen({
|
||||
super.key,
|
||||
required this.deck,
|
||||
required this.quizMode,
|
||||
required this.onCardReviewed,
|
||||
required this.useKanji,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomQuizScreen> createState() => CustomQuizScreenState();
|
||||
}
|
||||
|
||||
class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
List<CustomKanjiItem> _shuffledDeck = [];
|
||||
List<String> _options = [];
|
||||
@@ -27,17 +34,15 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
late FlutterTts _flutterTts;
|
||||
late AnimationController _shakeController;
|
||||
late Animation<double> _shakeAnimation;
|
||||
bool _useKanji = false;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shuffledDeck = widget.deck.toList()..shuffle();
|
||||
_initTts();
|
||||
_generateOptions();
|
||||
if (_shuffledDeck.isNotEmpty) {
|
||||
_generateOptions();
|
||||
}
|
||||
|
||||
_shakeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
@@ -51,6 +56,16 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomQuizScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.useKanji != oldWidget.useKanji) {
|
||||
setState(() {
|
||||
_generateOptions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void playAudio() {
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
_speak(_shuffledDeck[_currentIndex].characters);
|
||||
@@ -60,7 +75,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
void _initTts() async {
|
||||
_flutterTts = FlutterTts();
|
||||
await _flutterTts.setLanguage("ja-JP");
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
if (_shuffledDeck.isNotEmpty && widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||
_speak(_shuffledDeck[_currentIndex].characters);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +92,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||
_options = [currentItem.meaning];
|
||||
} else {
|
||||
_options = [_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters];
|
||||
_options = [widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters];
|
||||
}
|
||||
final otherItems = widget.deck
|
||||
.where((item) => item.characters != currentItem.characters)
|
||||
@@ -87,7 +102,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||
_options.add(otherItems[i].meaning);
|
||||
} else {
|
||||
_options.add(_useKanji && otherItems[i].kanji != null ? otherItems[i].kanji! : otherItems[i].characters);
|
||||
_options.add(widget.useKanji && otherItems[i].kanji != null ? otherItems[i].kanji! : otherItems[i].characters);
|
||||
}
|
||||
}
|
||||
_options.shuffle();
|
||||
@@ -96,10 +111,22 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
void _checkAnswer(String answer) {
|
||||
final currentItem = _shuffledDeck[_currentIndex];
|
||||
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||
? (_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
|
||||
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
|
||||
: currentItem.meaning;
|
||||
final isCorrect = answer == correctAnswer;
|
||||
|
||||
if (currentItem.useInterval) {
|
||||
if (isCorrect) {
|
||||
currentItem.srsLevel++;
|
||||
final interval = pow(2, currentItem.srsLevel).toInt();
|
||||
currentItem.nextReview = DateTime.now().add(Duration(hours: interval));
|
||||
} else {
|
||||
currentItem.srsLevel = max(0, currentItem.srsLevel - 1);
|
||||
currentItem.nextReview = DateTime.now().add(const Duration(hours: 1));
|
||||
}
|
||||
widget.onCardReviewed(currentItem);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_answered = true;
|
||||
_correct = isCorrect;
|
||||
@@ -139,91 +166,65 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (_shuffledDeck.isEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: const Center(
|
||||
child: Text('No cards in the deck!'),
|
||||
),
|
||||
return const Center(
|
||||
child: Text('Review session complete!'),
|
||||
);
|
||||
}
|
||||
|
||||
final currentItem = _shuffledDeck[_currentIndex];
|
||||
final question = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||
? currentItem.meaning
|
||||
: (_useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
|
||||
: (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Quiz'),
|
||||
actions: [
|
||||
if (widget.quizMode != CustomQuizMode.listeningComprehension)
|
||||
Row(
|
||||
children: [
|
||||
const Text('Kanji'),
|
||||
Switch(
|
||||
value: _useKanji,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useKanji = value;
|
||||
_generateOptions();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.volume_up, size: 64),
|
||||
onPressed: () => _speak(currentItem.characters),
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => _speak(question),
|
||||
child: Text(
|
||||
question,
|
||||
style: const TextStyle(fontSize: 48),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (_answered)
|
||||
Text(
|
||||
_correct! ? 'Correct!' : 'Incorrect, try again!',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: _correct! ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
AnimatedBuilder(
|
||||
animation: _shakeAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_shakeAnimation.value * 10, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: OptionsGrid(
|
||||
options: _options,
|
||||
onSelected: _onOptionSelected,
|
||||
),
|
||||
),
|
||||
if (_answered && _correct!)
|
||||
ElevatedButton(
|
||||
onPressed: _nextQuestion,
|
||||
child: const Text('Next'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.quizMode == CustomQuizMode.listeningComprehension)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.volume_up, size: 64),
|
||||
onPressed: () => _speak(currentItem.characters),
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => _speak(question),
|
||||
child: Text(
|
||||
question,
|
||||
style: const TextStyle(fontSize: 48),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (_answered)
|
||||
Text(
|
||||
_correct! ? 'Correct!' : 'Incorrect, try again!',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: _correct! ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
AnimatedBuilder(
|
||||
animation: _shakeAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_shakeAnimation.value * 10, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: OptionsGrid(
|
||||
options: _options,
|
||||
onSelected: _onOptionSelected,
|
||||
),
|
||||
),
|
||||
if (_answered && _correct!)
|
||||
ElevatedButton(
|
||||
onPressed: _nextQuestion,
|
||||
child: const Text('Next'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
||||
late TabController _tabController;
|
||||
final _deckRepository = CustomDeckRepository();
|
||||
List<CustomKanjiItem> _deck = [];
|
||||
List<CustomKanjiItem> _reviewDeck = [];
|
||||
bool _useKanji = false;
|
||||
final _quizScreenKeys = [
|
||||
GlobalKey<CustomQuizScreenState>(),
|
||||
GlobalKey<CustomQuizScreenState>(),
|
||||
@@ -30,6 +32,7 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
||||
final key = _quizScreenKeys[_tabController.index];
|
||||
key.currentState?.playAudio();
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
_loadDeck();
|
||||
}
|
||||
@@ -42,16 +45,52 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
||||
|
||||
Future<void> _loadDeck() async {
|
||||
final deck = await _deckRepository.getCustomDeck();
|
||||
final now = DateTime.now();
|
||||
final reviewDeck = deck.where((item) {
|
||||
if (!item.useInterval) {
|
||||
return true;
|
||||
}
|
||||
return item.nextReview == null || item.nextReview!.isBefore(now);
|
||||
}).toList();
|
||||
|
||||
setState(() {
|
||||
_deck = deck;
|
||||
_reviewDeck = reviewDeck;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateCard(CustomKanjiItem item) async {
|
||||
final index = _deck.indexWhere((element) => element.characters == item.characters);
|
||||
if (index != -1) {
|
||||
setState(() {
|
||||
_deck[index] = item;
|
||||
_reviewDeck.removeWhere((element) => element.characters == item.characters);
|
||||
});
|
||||
await _deckRepository.saveDeck(_deck);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Custom SRS'),
|
||||
actions: [
|
||||
if (_tabController.index != 2)
|
||||
Row(
|
||||
children: [
|
||||
const Text('Kanji'),
|
||||
Switch(
|
||||
value: _useKanji,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useKanji = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
@@ -62,13 +101,33 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
||||
),
|
||||
),
|
||||
body: _deck.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: TabBarView(
|
||||
? const Center(child: Text('Add cards to start quizzing!'))
|
||||
: _reviewDeck.isEmpty
|
||||
? const Center(child: Text('No cards due for review.'))
|
||||
: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
CustomQuizScreen(key: _quizScreenKeys[0], deck: _deck, quizMode: CustomQuizMode.japaneseToEnglish),
|
||||
CustomQuizScreen(key: _quizScreenKeys[1], deck: _deck, quizMode: CustomQuizMode.englishToJapanese),
|
||||
CustomQuizScreen(key: _quizScreenKeys[2], deck: _deck, quizMode: CustomQuizMode.listeningComprehension),
|
||||
CustomQuizScreen(
|
||||
key: _quizScreenKeys[0],
|
||||
deck: _reviewDeck,
|
||||
quizMode: CustomQuizMode.japaneseToEnglish,
|
||||
onCardReviewed: _updateCard,
|
||||
useKanji: _useKanji,
|
||||
),
|
||||
CustomQuizScreen(
|
||||
key: _quizScreenKeys[1],
|
||||
deck: _reviewDeck,
|
||||
quizMode: CustomQuizMode.englishToJapanese,
|
||||
onCardReviewed: _updateCard,
|
||||
useKanji: _useKanji,
|
||||
),
|
||||
CustomQuizScreen(
|
||||
key: _quizScreenKeys[2],
|
||||
deck: _reviewDeck,
|
||||
quizMode: CustomQuizMode.listeningComprehension,
|
||||
onCardReviewed: _updateCard,
|
||||
useKanji: _useKanji,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
|
||||
@@ -24,94 +24,108 @@ class StartScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text(
|
||||
'Kanji Quiz',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const VocabScreen()),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text(
|
||||
'Vocabulary Quiz',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const BrowseScreen()),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.deepPurpleAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text(
|
||||
'Browse Items',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const CustomSrsScreen()),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.greenAccent,
|
||||
foregroundColor: Colors.black,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text(
|
||||
'Custom SRS',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF121212),
|
||||
Colors.grey[900]!,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(16),
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.8,
|
||||
children: [
|
||||
_buildModeCard(
|
||||
context,
|
||||
title: 'Kanji Quiz',
|
||||
icon: Icons.extension,
|
||||
description: 'Test your knowledge of kanji characters.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
context,
|
||||
title: 'Vocabulary Quiz',
|
||||
icon: Icons.school,
|
||||
description: 'Practice vocabulary from your WaniKani deck.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const VocabScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
context,
|
||||
title: 'Browse Items',
|
||||
icon: Icons.grid_view,
|
||||
description: 'Look through your kanji and vocabulary decks.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const BrowseScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildModeCard(
|
||||
context,
|
||||
title: 'Custom SRS',
|
||||
icon: Icons.create,
|
||||
description: 'Create and study your own custom flashcards.',
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const CustomSrsScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeCard(BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required String description,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,10 +19,25 @@ class CustomDeckRepository {
|
||||
Future<void> addCard(CustomKanjiItem item) async {
|
||||
final deck = await getCustomDeck();
|
||||
deck.add(item);
|
||||
await _saveDeck(deck);
|
||||
await saveDeck(deck);
|
||||
}
|
||||
|
||||
Future<void> _saveDeck(List<CustomKanjiItem> deck) async {
|
||||
Future<void> updateCard(CustomKanjiItem item) async {
|
||||
final deck = await getCustomDeck();
|
||||
final index = deck.indexWhere((element) => element.characters == item.characters);
|
||||
if (index != -1) {
|
||||
deck[index] = item;
|
||||
await saveDeck(deck);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteCard(CustomKanjiItem item) async {
|
||||
final deck = await getCustomDeck();
|
||||
deck.removeWhere((element) => element.characters == item.characters);
|
||||
await saveDeck(deck);
|
||||
}
|
||||
|
||||
Future<void> saveDeck(List<CustomKanjiItem> deck) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = deck.map((item) => item.toJson()).toList();
|
||||
await prefs.setString(_key, json.encode(jsonList));
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:sqflite/sqflite.dart';
|
||||
import '../models/kanji_item.dart';
|
||||
import '../api/wk_client.dart';
|
||||
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
class DeckRepository {
|
||||
Database? _db;
|
||||
String? _apiKey;
|
||||
@@ -98,6 +100,12 @@ class DeckRepository {
|
||||
}
|
||||
|
||||
Future<String?> loadApiKey() async {
|
||||
final envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
||||
if (envApiKey != null && envApiKey.isNotEmpty) {
|
||||
_apiKey = envApiKey;
|
||||
return _apiKey;
|
||||
}
|
||||
|
||||
final db = await _openDb();
|
||||
final rows = await db.query(
|
||||
'settings',
|
||||
|
||||
@@ -302,6 +302,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_dotenv:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_dotenv
|
||||
sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies:
|
||||
http: ^1.5.0
|
||||
kana_kit: ^2.1.1
|
||||
flutter_tts: ^3.8.5
|
||||
flutter_dotenv: ^5.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -34,3 +35,4 @@ flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/sfx/confirm.mp3
|
||||
- .env
|
||||
|
||||
Reference in New Issue
Block a user