themes
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hirameki_srs/src/models/theme_model.dart';
|
||||||
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
|
import 'package:hirameki_srs/src/services/vocab_deck_repository.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
@@ -9,16 +10,15 @@ void main() async {
|
|||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
try {
|
try {
|
||||||
await dotenv.load(fileName: ".env");
|
await dotenv.load(fileName: ".env");
|
||||||
} catch (e) {
|
// No need to catch because the file is only needed for dev
|
||||||
// It's okay if the .env file is not found.
|
} catch (_) {}
|
||||||
// This is expected in release builds.
|
|
||||||
}
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<DeckRepository>(create: (_) => DeckRepository()),
|
Provider<DeckRepository>(create: (_) => DeckRepository()),
|
||||||
Provider<VocabDeckRepository>(create: (_) => VocabDeckRepository()),
|
Provider<VocabDeckRepository>(create: (_) => VocabDeckRepository()),
|
||||||
|
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
|
||||||
],
|
],
|
||||||
child: const WkApp(),
|
child: const WkApp(),
|
||||||
),
|
),
|
||||||
@@ -30,29 +30,15 @@ class WkApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ThemeModel>(
|
||||||
|
builder: (context, themeModel, child) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Hirameki SRS',
|
title: 'Hirameki SRS',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: themeModel.currentTheme,
|
||||||
colorScheme: const ColorScheme(
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
primary: Color(0xFF90CAF9), // Light blue for primary elements
|
|
||||||
onPrimary: Colors.black,
|
|
||||||
secondary: Color(0xFFBBDEFB), // Slightly lighter blue for secondary elements
|
|
||||||
onSecondary: Colors.black,
|
|
||||||
tertiary: Color(0xFFA5D6A7), // Light green for success/correct states
|
|
||||||
onTertiary: Colors.black,
|
|
||||||
error: Color(0xFFEF9A9A), // Light red for error states
|
|
||||||
onError: Colors.black,
|
|
||||||
surface: Color(0xFF121212), // Very dark gray
|
|
||||||
onSurface: Colors.white,
|
|
||||||
surfaceContainer: Color(0xFF1E1E1E), // Slightly lighter dark gray
|
|
||||||
surfaceContainerHighest: Color(0xFF424242), // A distinct dark gray for surface variants
|
|
||||||
onSurfaceVariant: Colors.white70,
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: const StartScreen(),
|
home: const StartScreen(),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,16 @@ class WkClient {
|
|||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
final String base = 'https://api.wanikani.com/v2';
|
final String base = 'https://api.wanikani.com/v2';
|
||||||
|
|
||||||
WkClient(this.apiKey) : headers = {'Authorization': 'Bearer $apiKey', 'Wanikani-Revision': '20170710', 'Accept': 'application/json'};
|
WkClient(this.apiKey)
|
||||||
|
: headers = {
|
||||||
|
'Authorization': 'Bearer $apiKey',
|
||||||
|
'Wanikani-Revision': '20170710',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> fetchAllAssignments({List<String>? subjectTypes}) async {
|
Future<List<Map<String, dynamic>>> fetchAllAssignments({
|
||||||
|
List<String>? subjectTypes,
|
||||||
|
}) async {
|
||||||
final out = <Map<String, dynamic>>[];
|
final out = <Map<String, dynamic>>[];
|
||||||
String url = '$base/assignments?page=1';
|
String url = '$base/assignments?page=1';
|
||||||
if (subjectTypes != null && subjectTypes.isNotEmpty) {
|
if (subjectTypes != null && subjectTypes.isNotEmpty) {
|
||||||
@@ -30,7 +37,9 @@ class WkClient {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> fetchAllSubjects({List<String>? types}) async {
|
Future<List<Map<String, dynamic>>> fetchAllSubjects({
|
||||||
|
List<String>? types,
|
||||||
|
}) async {
|
||||||
final out = <Map<String, dynamic>>[];
|
final out = <Map<String, dynamic>>[];
|
||||||
String url = '$base/subjects';
|
String url = '$base/subjects';
|
||||||
if (types != null && types.isNotEmpty) {
|
if (types != null && types.isNotEmpty) {
|
||||||
@@ -56,7 +65,10 @@ class WkClient {
|
|||||||
final out = <Map<String, dynamic>>[];
|
final out = <Map<String, dynamic>>[];
|
||||||
const batch = 100;
|
const batch = 100;
|
||||||
for (var i = 0; i < ids.length; i += batch) {
|
for (var i = 0; i < ids.length; i += batch) {
|
||||||
final chunk = ids.sublist(i, i + batch > ids.length ? ids.length : i + batch);
|
final chunk = ids.sublist(
|
||||||
|
i,
|
||||||
|
i + batch > ids.length ? ids.length : i + batch,
|
||||||
|
);
|
||||||
String url = '$base/subjects?ids=${chunk.join(',')}&page=1';
|
String url = '$base/subjects?ids=${chunk.join(',')}&page=1';
|
||||||
while (true) {
|
while (true) {
|
||||||
final resp = await http.get(Uri.parse(url), headers: headers);
|
final resp = await http.get(Uri.parse(url), headers: headers);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
class CustomKanjiItem {
|
class CustomKanjiItem {
|
||||||
final String characters;
|
final String characters;
|
||||||
final String meaning;
|
final String meaning;
|
||||||
@@ -25,7 +24,9 @@ class CustomKanjiItem {
|
|||||||
srsData.listeningComprehensionNextReview ??= oldNextReview;
|
srsData.listeningComprehensionNextReview ??= oldNextReview;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DateTime? nextReview = json['nextReview'] != null ? DateTime.parse(json['nextReview'] as String) : null;
|
DateTime? nextReview = json['nextReview'] != null
|
||||||
|
? DateTime.parse(json['nextReview'] as String)
|
||||||
|
: null;
|
||||||
srsData = SrsData(
|
srsData = SrsData(
|
||||||
japaneseToEnglish: json['srsLevel'] as int? ?? 0,
|
japaneseToEnglish: json['srsLevel'] as int? ?? 0,
|
||||||
japaneseToEnglishNextReview: nextReview,
|
japaneseToEnglishNextReview: nextReview,
|
||||||
@@ -76,22 +77,32 @@ class SrsData {
|
|||||||
factory SrsData.fromJson(Map<String, dynamic> json) {
|
factory SrsData.fromJson(Map<String, dynamic> json) {
|
||||||
return SrsData(
|
return SrsData(
|
||||||
japaneseToEnglish: json['japaneseToEnglish'] as int? ?? 0,
|
japaneseToEnglish: json['japaneseToEnglish'] as int? ?? 0,
|
||||||
japaneseToEnglishNextReview: json['japaneseToEnglishNextReview'] != null ? DateTime.parse(json['japaneseToEnglishNextReview'] as String) : null,
|
japaneseToEnglishNextReview: json['japaneseToEnglishNextReview'] != null
|
||||||
|
? DateTime.parse(json['japaneseToEnglishNextReview'] as String)
|
||||||
|
: null,
|
||||||
englishToJapanese: json['englishToJapanese'] as int? ?? 0,
|
englishToJapanese: json['englishToJapanese'] as int? ?? 0,
|
||||||
englishToJapaneseNextReview: json['englishToJapaneseNextReview'] != null ? DateTime.parse(json['englishToJapaneseNextReview'] as String) : null,
|
englishToJapaneseNextReview: json['englishToJapaneseNextReview'] != null
|
||||||
|
? DateTime.parse(json['englishToJapaneseNextReview'] as String)
|
||||||
|
: null,
|
||||||
listeningComprehension: json['listeningComprehension'] as int? ?? 0,
|
listeningComprehension: json['listeningComprehension'] as int? ?? 0,
|
||||||
listeningComprehensionNextReview: json['listeningComprehensionNextReview'] != null ? DateTime.parse(json['listeningComprehensionNextReview'] as String) : null,
|
listeningComprehensionNextReview:
|
||||||
|
json['listeningComprehensionNextReview'] != null
|
||||||
|
? DateTime.parse(json['listeningComprehensionNextReview'] as String)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'japaneseToEnglish': japaneseToEnglish,
|
'japaneseToEnglish': japaneseToEnglish,
|
||||||
'japaneseToEnglishNextReview': japaneseToEnglishNextReview?.toIso8601String(),
|
'japaneseToEnglishNextReview': japaneseToEnglishNextReview
|
||||||
|
?.toIso8601String(),
|
||||||
'englishToJapanese': englishToJapanese,
|
'englishToJapanese': englishToJapanese,
|
||||||
'englishToJapaneseNextReview': englishToJapaneseNextReview?.toIso8601String(),
|
'englishToJapaneseNextReview': englishToJapaneseNextReview
|
||||||
|
?.toIso8601String(),
|
||||||
'listeningComprehension': listeningComprehension,
|
'listeningComprehension': listeningComprehension,
|
||||||
'listeningComprehensionNextReview': listeningComprehensionNextReview?.toIso8601String(),
|
'listeningComprehensionNextReview': listeningComprehensionNextReview
|
||||||
|
?.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ enum QuizMode { kanjiToEnglish, englishToKanji, reading }
|
|||||||
class SrsItem {
|
class SrsItem {
|
||||||
final int kanjiId;
|
final int kanjiId;
|
||||||
final QuizMode quizMode;
|
final QuizMode quizMode;
|
||||||
final String? readingType; // 'onyomi' or 'kunyomi'
|
final String? readingType;
|
||||||
int srsStage;
|
int srsStage;
|
||||||
DateTime lastAsked;
|
DateTime lastAsked;
|
||||||
|
|
||||||
@@ -116,13 +116,14 @@ class VocabularyItem {
|
|||||||
final List<PronunciationAudio> pronunciationAudios;
|
final List<PronunciationAudio> pronunciationAudios;
|
||||||
final Map<String, VocabSrsItem> srsItems = {};
|
final Map<String, VocabSrsItem> srsItems = {};
|
||||||
|
|
||||||
VocabularyItem(
|
VocabularyItem({
|
||||||
{required this.id,
|
required this.id,
|
||||||
required this.level,
|
required this.level,
|
||||||
required this.characters,
|
required this.characters,
|
||||||
required this.meanings,
|
required this.meanings,
|
||||||
required this.readings,
|
required this.readings,
|
||||||
required this.pronunciationAudios});
|
required this.pronunciationAudios,
|
||||||
|
});
|
||||||
|
|
||||||
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
|
factory VocabularyItem.fromSubject(Map<String, dynamic> subj) {
|
||||||
final int id = subj['id'] as int;
|
final int id = subj['id'] as int;
|
||||||
@@ -152,10 +153,7 @@ class VocabularyItem {
|
|||||||
final gender = metadata?['gender'] as String?;
|
final gender = metadata?['gender'] as String?;
|
||||||
|
|
||||||
if (url != null && gender != null) {
|
if (url != null && gender != null) {
|
||||||
pronunciationAudios.add(PronunciationAudio(
|
pronunciationAudios.add(PronunciationAudio(url: url, gender: gender));
|
||||||
url: url,
|
|
||||||
gender: gender,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +164,7 @@ class VocabularyItem {
|
|||||||
characters: characters,
|
characters: characters,
|
||||||
meanings: meanings,
|
meanings: meanings,
|
||||||
readings: readings,
|
readings: readings,
|
||||||
pronunciationAudios: pronunciationAudios);
|
pronunciationAudios: pronunciationAudios,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
lib/src/models/theme_model.dart
Normal file
13
lib/src/models/theme_model.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hirameki_srs/src/themes.dart';
|
||||||
|
|
||||||
|
class ThemeModel extends ChangeNotifier {
|
||||||
|
ThemeData _currentTheme = Themes.dark;
|
||||||
|
|
||||||
|
ThemeData get currentTheme => _currentTheme;
|
||||||
|
|
||||||
|
void setTheme(ThemeData theme) {
|
||||||
|
_currentTheme = theme;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kana_kit/kana_kit.dart';
|
import 'package:kana_kit/kana_kit.dart';
|
||||||
import '../models/custom_kanji_item.dart';
|
import '../models/custom_kanji_item.dart';
|
||||||
@@ -61,7 +60,9 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
final newItem = CustomKanjiItem(
|
final newItem = CustomKanjiItem(
|
||||||
characters: _japaneseController.text,
|
characters: _japaneseController.text,
|
||||||
meaning: _englishController.text,
|
meaning: _englishController.text,
|
||||||
kanji: _kanjiController.text.trim().isNotEmpty ? _kanjiController.text.trim() : null,
|
kanji: _kanjiController.text.trim().isNotEmpty
|
||||||
|
? _kanjiController.text.trim()
|
||||||
|
: null,
|
||||||
useInterval: _useInterval,
|
useInterval: _useInterval,
|
||||||
srsData: srsData,
|
srsData: srsData,
|
||||||
);
|
);
|
||||||
@@ -73,9 +74,7 @@ class _AddCardScreenState extends State<AddCardScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Add New Card')),
|
||||||
title: const Text('Add New Card'),
|
|
||||||
),
|
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hirameki_srs/src/themes.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import '../models/kanji_item.dart';
|
import '../models/kanji_item.dart';
|
||||||
@@ -18,7 +19,8 @@ class BrowseScreen extends StatefulWidget {
|
|||||||
State<BrowseScreen> createState() => _BrowseScreenState();
|
State<BrowseScreen> createState() => _BrowseScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderStateMixin {
|
class _BrowseScreenState extends State<BrowseScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
late PageController _kanjiPageController;
|
late PageController _kanjiPageController;
|
||||||
late PageController _vocabPageController;
|
late PageController _vocabPageController;
|
||||||
@@ -50,7 +52,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
_vocabPageController = PageController();
|
_vocabPageController = PageController();
|
||||||
|
|
||||||
_tabController.addListener(() {
|
_tabController.addListener(() {
|
||||||
setState(() {}); // Rebuild to update the level selector
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
_kanjiPageController.addListener(() {
|
_kanjiPageController.addListener(() {
|
||||||
@@ -94,13 +96,17 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text('WaniKani API key is not set.', style: TextStyle(color: Colors.white)),
|
Text(
|
||||||
|
'WaniKani API key is not set.',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
_loadDecks();
|
_loadDecks();
|
||||||
},
|
},
|
||||||
child: const Text('Go to Settings'),
|
child: const Text('Go to Settings'),
|
||||||
@@ -115,9 +121,9 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const CircularProgressIndicator(color: Colors.blueAccent),
|
CircularProgressIndicator(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(_status, style: const TextStyle(color: Colors.white)),
|
Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -128,8 +134,11 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
|
|
||||||
Widget _buildCustomSrsTab() {
|
Widget _buildCustomSrsTab() {
|
||||||
if (_customDeck.isEmpty) {
|
if (_customDeck.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text('No custom cards yet.', style: TextStyle(color: Colors.white)),
|
child: Text(
|
||||||
|
'No custom cards yet.',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _buildCustomGridView(_customDeck);
|
return _buildCustomGridView(_customDeck);
|
||||||
@@ -139,11 +148,14 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
Map<int, List<dynamic>> groupedItems,
|
Map<int, List<dynamic>> groupedItems,
|
||||||
List<int> sortedLevels,
|
List<int> sortedLevels,
|
||||||
PageController pageController,
|
PageController pageController,
|
||||||
Widget Function(List<dynamic>) buildPageContent) {
|
Widget Function(List<dynamic>) buildPageContent,
|
||||||
|
) {
|
||||||
if (sortedLevels.isEmpty) {
|
if (sortedLevels.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child:
|
child: Text(
|
||||||
Text('No items to display.', style: TextStyle(color: Colors.white)),
|
'No items to display.',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +172,11 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Level $level',
|
'Level $level',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: Colors.white,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: buildPageContent(levelItems)),
|
Expanded(child: buildPageContent(levelItems)),
|
||||||
@@ -183,7 +196,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
color: const Color(0xFF1F1F1F),
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
height: 60,
|
height: 60,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@@ -196,11 +209,17 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
controller.animateToPage(
|
||||||
|
index,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: isSelected ? Colors.blueAccent : const Color(0xFF333333),
|
backgroundColor: isSelected
|
||||||
foregroundColor: Colors.white,
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
),
|
),
|
||||||
@@ -266,7 +285,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _showVocabDetailsDialog(context, item),
|
onTap: () => _showVocabDetailsDialog(context, item),
|
||||||
child: Card(
|
child: Card(
|
||||||
color: const Color(0xFF1E1E1E),
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
@@ -275,7 +294,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.characters,
|
item.characters,
|
||||||
style: const TextStyle(fontSize: 24, color: Colors.white),
|
style: TextStyle(fontSize: 24, color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -286,7 +305,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.meanings.join(', '),
|
item.meanings.join(', '),
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -327,7 +346,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
color: const Color(0xFF1E1E1E),
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -335,7 +354,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.characters,
|
item.characters,
|
||||||
style: const TextStyle(fontSize: 32, color: Colors.white),
|
style: TextStyle(fontSize: 32, color: Theme.of(context).colorScheme.onSurface),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -354,8 +373,8 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: level / 9.0, // Max SRS level is 9
|
value: level / 9.0,
|
||||||
backgroundColor: Colors.grey[800],
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
_getColorForSrsLevel(level),
|
_getColorForSrsLevel(level),
|
||||||
),
|
),
|
||||||
@@ -366,13 +385,17 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color _getColorForSrsLevel(int level) {
|
Color _getColorForSrsLevel(int level) {
|
||||||
if (level >= 9) return Colors.purple;
|
final srsColors = Theme.of(context).srsColors;
|
||||||
if (level >= 8) return Colors.blue;
|
if (level >= 9) return srsColors.level9;
|
||||||
if (level >= 7) return Colors.lightBlue;
|
if (level >= 8) return srsColors.level8;
|
||||||
if (level >= 5) return Colors.green;
|
if (level >= 7) return srsColors.level7;
|
||||||
if (level >= 3) return Colors.yellow;
|
if (level >= 6) return srsColors.level6;
|
||||||
if (level >= 1) return Colors.orange;
|
if (level >= 5) return srsColors.level5;
|
||||||
return Colors.red;
|
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) {
|
void _showReadingsDialog(KanjiItem kanji) {
|
||||||
@@ -404,12 +427,12 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (dialogContext) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Details for ${kanji.characters}',
|
'Details for ${kanji.characters}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -418,50 +441,56 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Level: ${kanji.level}',
|
'Level: ${kanji.level}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (kanji.meanings.isNotEmpty)
|
if (kanji.meanings.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
'Meanings: ${kanji.meanings.join(', ')}',
|
'Meanings: ${kanji.meanings.join(', ')}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (kanji.onyomi.isNotEmpty)
|
if (kanji.onyomi.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
'On\'yomi: ${kanji.onyomi.join(', ')}',
|
'On\'yomi: ${kanji.onyomi.join(', ')}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
if (kanji.kunyomi.isNotEmpty)
|
if (kanji.kunyomi.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
'Kun\'yomi: ${kanji.kunyomi.join(', ')}',
|
'Kun\'yomi: ${kanji.kunyomi.join(', ')}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty)
|
if (kanji.onyomi.isEmpty && kanji.kunyomi.isEmpty)
|
||||||
const Text(
|
Text(
|
||||||
'No readings available.',
|
'No readings available.',
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(color: Colors.grey),
|
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'SRS Scores:',
|
'SRS Scores:',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white, fontWeight: FontWeight.bold),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
...srsScores.entries.map((entry) => Text(
|
),
|
||||||
|
...srsScores.entries.map(
|
||||||
|
(entry) => Text(
|
||||||
' ${entry.key}: ${entry.value}',
|
' ${entry.key}: ${entry.value}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
child: const Text('Close',
|
child: Text(
|
||||||
style: TextStyle(color: Colors.blueAccent)),
|
'Close',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -473,8 +502,10 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
try {
|
try {
|
||||||
final kanjiRepo = Provider.of<DeckRepository>(context, listen: false);
|
final kanjiRepo = Provider.of<DeckRepository>(context, listen: false);
|
||||||
final vocabRepo =
|
final vocabRepo = Provider.of<VocabDeckRepository>(
|
||||||
Provider.of<VocabDeckRepository>(context, listen: false);
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
await kanjiRepo.loadApiKey();
|
await kanjiRepo.loadApiKey();
|
||||||
final apiKey = kanjiRepo.apiKey;
|
final apiKey = kanjiRepo.apiKey;
|
||||||
|
|
||||||
@@ -537,9 +568,9 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar:
|
appBar: _isSelectionMode
|
||||||
_isSelectionMode ? _buildSelectionAppBar() : _buildDefaultAppBar(),
|
? _buildSelectionAppBar()
|
||||||
backgroundColor: const Color(0xFF121212),
|
: _buildDefaultAppBar(),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -551,15 +582,16 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
_kanjiByLevel,
|
_kanjiByLevel,
|
||||||
_kanjiSortedLevels,
|
_kanjiSortedLevels,
|
||||||
_kanjiPageController,
|
_kanjiPageController,
|
||||||
(items) => _buildGridView(items.cast<KanjiItem>())),
|
(items) => _buildGridView(items.cast<KanjiItem>()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_buildWaniKaniTab(
|
_buildWaniKaniTab(
|
||||||
_buildPaginatedView(
|
_buildPaginatedView(
|
||||||
_vocabByLevel,
|
_vocabByLevel,
|
||||||
_vocabSortedLevels,
|
_vocabSortedLevels,
|
||||||
_vocabPageController,
|
_vocabPageController,
|
||||||
(items) =>
|
(items) => _buildListView(items.cast<VocabularyItem>()),
|
||||||
_buildListView(items.cast<VocabularyItem>())),
|
),
|
||||||
),
|
),
|
||||||
_buildCustomSrsTab(),
|
_buildCustomSrsTab(),
|
||||||
],
|
],
|
||||||
@@ -577,9 +609,10 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
floatingActionButton: _tabController.index == 2
|
floatingActionButton: _tabController.index == 2
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(
|
||||||
MaterialPageRoute(builder: (_) => AddCardScreen()),
|
context,
|
||||||
);
|
).push(MaterialPageRoute(builder: (_) => AddCardScreen()));
|
||||||
|
if (!mounted) return;
|
||||||
_loadCustomDeck();
|
_loadCustomDeck();
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
@@ -615,14 +648,8 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
),
|
),
|
||||||
title: Text('${_selectedItems.length} selected'),
|
title: Text('${_selectedItems.length} selected'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(icon: const Icon(Icons.select_all), onPressed: _selectAll),
|
||||||
icon: const Icon(Icons.select_all),
|
IconButton(icon: const Icon(Icons.delete), onPressed: _deleteSelected),
|
||||||
onPressed: _selectAll,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: _deleteSelected,
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(_toggleIntervalIcon),
|
icon: Icon(_toggleIntervalIcon),
|
||||||
onPressed: _toggleIntervalForSelected,
|
onPressed: _toggleIntervalForSelected,
|
||||||
@@ -652,28 +679,30 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
void _deleteSelected() {
|
void _deleteSelected() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Delete Selected'),
|
title: const Text('Delete Selected'),
|
||||||
content:
|
content: Text(
|
||||||
Text('Are you sure you want to delete ${_selectedItems.length} cards?'),
|
'Are you sure you want to delete ${_selectedItems.length} cards?',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
final navigator = Navigator.of(context);
|
Navigator.of(dialogContext).pop();
|
||||||
|
() async {
|
||||||
for (final item in _selectedItems) {
|
for (final item in _selectedItems) {
|
||||||
await _customDeckRepository.deleteCard(item);
|
await _customDeckRepository.deleteCard(item);
|
||||||
}
|
}
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSelectionMode = false;
|
_isSelectionMode = false;
|
||||||
_selectedItems.clear();
|
_selectedItems.clear();
|
||||||
});
|
});
|
||||||
await _loadCustomDeck();
|
_loadCustomDeck();
|
||||||
if (!mounted) return;
|
}();
|
||||||
navigator.pop();
|
|
||||||
},
|
},
|
||||||
child: const Text('Delete'),
|
child: const Text('Delete'),
|
||||||
),
|
),
|
||||||
@@ -688,8 +717,9 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
}
|
}
|
||||||
final bool targetState = _selectedItems.any((item) => !item.useInterval);
|
final bool targetState = _selectedItems.any((item) => !item.useInterval);
|
||||||
|
|
||||||
final selectedCharacters =
|
final selectedCharacters = _selectedItems
|
||||||
_selectedItems.map((item) => item.characters).toSet();
|
.map((item) => item.characters)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
final List<CustomKanjiItem> updatedItems = [];
|
final List<CustomKanjiItem> updatedItems = [];
|
||||||
for (final item in _selectedItems) {
|
for (final item in _selectedItems) {
|
||||||
@@ -765,13 +795,13 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: isSelected
|
side: isSelected
|
||||||
? const BorderSide(color: Colors.blue, width: 2.0)
|
? BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Colors.blue.withAlpha((255 * 0.5).round())
|
? Theme.of(context).colorScheme.primary.withAlpha((255 * 0.5).round())
|
||||||
: const Color(0xFF1E1E1E),
|
: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -785,27 +815,34 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
item.kanji?.isNotEmpty == true
|
item.kanji?.isNotEmpty == true
|
||||||
? item.kanji!
|
? item.kanji!
|
||||||
: item.characters,
|
: item.characters,
|
||||||
style:
|
style: TextStyle(
|
||||||
const TextStyle(fontSize: 32, color: Colors.white),
|
fontSize: 32,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
item.meaning,
|
item.meaning,
|
||||||
style:
|
style: TextStyle(
|
||||||
const TextStyle(color: Colors.grey, fontSize: 16),
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
final avgSrs = (item.srsData.japaneseToEnglish +
|
builder: (context) {
|
||||||
|
final avgSrs =
|
||||||
|
(item.srsData.japaneseToEnglish +
|
||||||
item.srsData.englishToJapanese +
|
item.srsData.englishToJapanese +
|
||||||
item.srsData.listeningComprehension) /
|
item.srsData.listeningComprehension) /
|
||||||
3;
|
3;
|
||||||
return _buildSrsIndicator(avgSrs.round());
|
return _buildSrsIndicator(avgSrs.round());
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -813,11 +850,7 @@ class _BrowseScreenState extends State<BrowseScreen> with SingleTickerProviderSt
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: 4,
|
top: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
child: Icon(
|
child: Icon(Icons.timer, color: Theme.of(context).colorScheme.tertiary, size: 16),
|
||||||
Icons.timer,
|
|
||||||
color: Colors.green,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -848,9 +881,11 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchExampleSentences() async {
|
Future<void> _fetchExampleSentences() async {
|
||||||
|
final theme = Theme.of(context);
|
||||||
try {
|
try {
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}');
|
'https://jisho.org/api/v1/search/words?keyword=${Uri.encodeComponent(widget.vocab.characters)}',
|
||||||
|
);
|
||||||
final response = await http.get(uri);
|
final response = await http.get(uri);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
@@ -861,15 +896,24 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
(result['japanese'] as List).isNotEmpty &&
|
(result['japanese'] as List).isNotEmpty &&
|
||||||
result['senses'] != null &&
|
result['senses'] != null &&
|
||||||
(result['senses'] as List).isNotEmpty) {
|
(result['senses'] as List).isNotEmpty) {
|
||||||
final japaneseWord = result['japanese'][0]['word'] ?? result['japanese'][0]['reading'];
|
final japaneseWord =
|
||||||
final englishDefinition = result['senses'][0]['english_definitions'].join(', ');
|
result['japanese'][0]['word'] ??
|
||||||
|
result['japanese'][0]['reading'];
|
||||||
|
final englishDefinition =
|
||||||
|
result['senses'][0]['english_definitions'].join(', ');
|
||||||
if (japaneseWord != null && englishDefinition != null) {
|
if (japaneseWord != null && englishDefinition != null) {
|
||||||
sentences.add(
|
sentences.add(
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(japaneseWord, style: const TextStyle(color: Colors.white)),
|
Text(
|
||||||
Text(englishDefinition, style: const TextStyle(color: Colors.grey)),
|
japaneseWord,
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
englishDefinition,
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -879,7 +923,12 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sentences.isEmpty) {
|
if (sentences.isEmpty) {
|
||||||
sentences.add(const Text('No example sentences found.', style: TextStyle(color: Colors.white)));
|
sentences.add(
|
||||||
|
Text(
|
||||||
|
'No example sentences found.',
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -890,7 +939,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_exampleSentences = [
|
_exampleSentences = [
|
||||||
const Text('Failed to load example sentences.', style: TextStyle(color: Colors.red))
|
Text(
|
||||||
|
'Failed to load example sentences.',
|
||||||
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -899,7 +951,10 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_exampleSentences = [
|
_exampleSentences = [
|
||||||
const Text('Error loading example sentences.', style: TextStyle(color: Colors.red))
|
Text(
|
||||||
|
'Error loading example sentences.',
|
||||||
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -908,11 +963,7 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final srsScores = <String, int>{
|
final srsScores = <String, int>{'JP -> EN': 0, 'EN -> JP': 0, 'Audio': 0};
|
||||||
'JP -> EN': 0,
|
|
||||||
'EN -> JP': 0,
|
|
||||||
'Audio': 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (final entry in widget.vocab.srsItems.entries) {
|
for (final entry in widget.vocab.srsItems.entries) {
|
||||||
final srsItem = entry.value;
|
final srsItem = entry.value;
|
||||||
@@ -936,37 +987,39 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Level: ${widget.vocab.level}',
|
'Level: ${widget.vocab.level}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (widget.vocab.meanings.isNotEmpty)
|
if (widget.vocab.meanings.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
'Meanings: ${widget.vocab.meanings.join(', ')}',
|
'Meanings: ${widget.vocab.meanings.join(', ')}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (widget.vocab.readings.isNotEmpty)
|
if (widget.vocab.readings.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
'Readings: ${widget.vocab.readings.join(', ')}',
|
'Readings: ${widget.vocab.readings.join(', ')}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(color: Colors.grey),
|
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'SRS Scores:',
|
'SRS Scores:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
...srsScores.entries.map((entry) => Text(
|
...srsScores.entries.map(
|
||||||
|
(entry) => Text(
|
||||||
' ${entry.key}: ${entry.value}',
|
' ${entry.key}: ${entry.value}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(color: Colors.grey),
|
Divider(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'Example Sentences:',
|
'Example Sentences:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
..._exampleSentences,
|
..._exampleSentences,
|
||||||
],
|
],
|
||||||
@@ -978,18 +1031,21 @@ class _VocabDetailsDialogState extends State<_VocabDetailsDialog> {
|
|||||||
void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
|
void _showVocabDetailsDialog(BuildContext context, VocabularyItem vocab) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (dialogContext) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Details for ${vocab.characters}',
|
'Details for ${vocab.characters}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
content: _VocabDetailsDialog(vocab: vocab),
|
content: _VocabDetailsDialog(vocab: vocab),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
child: const Text('Close', style: TextStyle(color: Colors.blueAccent)),
|
child: Text(
|
||||||
|
'Close',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ class CustomCardDetailsScreen extends StatefulWidget {
|
|||||||
final CustomKanjiItem item;
|
final CustomKanjiItem item;
|
||||||
final CustomDeckRepository repository;
|
final CustomDeckRepository repository;
|
||||||
|
|
||||||
const CustomCardDetailsScreen(
|
const CustomCardDetailsScreen({
|
||||||
{super.key, required this.item, required this.repository});
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
required this.repository,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CustomCardDetailsScreen> createState() =>
|
State<CustomCardDetailsScreen> createState() =>
|
||||||
@@ -41,7 +44,9 @@ class _CustomCardDetailsScreenState extends State<CustomCardDetailsScreen> {
|
|||||||
final updatedItem = CustomKanjiItem(
|
final updatedItem = CustomKanjiItem(
|
||||||
characters: _japaneseController.text,
|
characters: _japaneseController.text,
|
||||||
meaning: _englishController.text,
|
meaning: _englishController.text,
|
||||||
kanji: _kanjiController.text.trim().isNotEmpty ? _kanjiController.text.trim() : null,
|
kanji: _kanjiController.text.trim().isNotEmpty
|
||||||
|
? _kanjiController.text.trim()
|
||||||
|
: null,
|
||||||
useInterval: _useInterval,
|
useInterval: _useInterval,
|
||||||
srsData: widget.item.srsData,
|
srsData: widget.item.srsData,
|
||||||
);
|
);
|
||||||
@@ -79,10 +84,7 @@ class _CustomCardDetailsScreenState extends State<CustomCardDetailsScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Edit Card'),
|
title: const Text('Edit Card'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(icon: const Icon(Icons.delete), onPressed: _deleteCard),
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: _deleteCard,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
@@ -111,10 +113,19 @@ class _CustomCardDetailsScreenState extends State<CustomCardDetailsScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text('SRS Levels', style: TextStyle(fontWeight: FontWeight.bold)),
|
const Text(
|
||||||
Text('Jpn→Eng: ${widget.item.srsData.japaneseToEnglish} (Next review: ${widget.item.srsData.japaneseToEnglishNextReview?.toString() ?? 'N/A'})'),
|
'SRS Levels',
|
||||||
Text('Eng→Jpn: ${widget.item.srsData.englishToJapanese} (Next review: ${widget.item.srsData.englishToJapaneseNextReview?.toString() ?? 'N/A'})'),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
Text('Listening: ${widget.item.srsData.listeningComprehension} (Next review: ${widget.item.srsData.listeningComprehensionNextReview?.toString() ?? 'N/A'})'),
|
),
|
||||||
|
Text(
|
||||||
|
'Jpn→Eng: ${widget.item.srsData.japaneseToEnglish} (Next review: ${widget.item.srsData.japaneseToEnglishNextReview?.toString() ?? 'N/A'})',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Eng→Jpn: ${widget.item.srsData.englishToJapanese} (Next review: ${widget.item.srsData.englishToJapaneseNextReview?.toString() ?? 'N/A'})',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Listening: ${widget.item.srsData.listeningComprehension} (Next review: ${widget.item.srsData.listeningComprehensionNextReview?.toString() ?? 'N/A'})',
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _saveChanges,
|
onPressed: _saveChanges,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import '../models/custom_kanji_item.dart';
|
|||||||
import '../widgets/options_grid.dart';
|
import '../widgets/options_grid.dart';
|
||||||
import '../widgets/kanji_card.dart';
|
import '../widgets/kanji_card.dart';
|
||||||
|
|
||||||
enum CustomQuizMode { japaneseToEnglish, englishToJapanese, listeningComprehension }
|
enum CustomQuizMode {
|
||||||
|
japaneseToEnglish,
|
||||||
|
englishToJapanese,
|
||||||
|
listeningComprehension,
|
||||||
|
}
|
||||||
|
|
||||||
class CustomQuizScreen extends StatefulWidget {
|
class CustomQuizScreen extends StatefulWidget {
|
||||||
final List<CustomKanjiItem> deck;
|
final List<CustomKanjiItem> deck;
|
||||||
@@ -53,10 +57,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||||
CurvedAnimation(
|
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
|
||||||
parent: _shakeController,
|
|
||||||
curve: Curves.elasticIn,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void playAudio() {
|
void playAudio() {
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension && _currentIndex < _shuffledDeck.length) {
|
if (widget.quizMode == CustomQuizMode.listeningComprehension &&
|
||||||
|
_currentIndex < _shuffledDeck.length) {
|
||||||
_speak(_shuffledDeck[_currentIndex].characters);
|
_speak(_shuffledDeck[_currentIndex].characters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,20 +103,30 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
|
|
||||||
void _generateOptions() {
|
void _generateOptions() {
|
||||||
final currentItem = _shuffledDeck[_currentIndex];
|
final currentItem = _shuffledDeck[_currentIndex];
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
||||||
|
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||||
_options = [currentItem.meaning];
|
_options = [currentItem.meaning];
|
||||||
} else {
|
} else {
|
||||||
_options = [widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters];
|
_options = [
|
||||||
|
widget.useKanji && currentItem.kanji != null
|
||||||
|
? currentItem.kanji!
|
||||||
|
: currentItem.characters,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
final otherItems = widget.deck
|
final otherItems = widget.deck
|
||||||
.where((item) => item.characters != currentItem.characters)
|
.where((item) => item.characters != currentItem.characters)
|
||||||
.toList();
|
.toList();
|
||||||
otherItems.shuffle();
|
otherItems.shuffle();
|
||||||
for (var i = 0; i < min(3, otherItems.length); i++) {
|
for (var i = 0; i < min(3, otherItems.length); i++) {
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension || widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
if (widget.quizMode == CustomQuizMode.listeningComprehension ||
|
||||||
|
widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||||
_options.add(otherItems[i].meaning);
|
_options.add(otherItems[i].meaning);
|
||||||
} else {
|
} else {
|
||||||
_options.add(widget.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();
|
_options.shuffle();
|
||||||
@@ -123,7 +135,9 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
void _checkAnswer(String answer) async {
|
void _checkAnswer(String answer) async {
|
||||||
final currentItem = _shuffledDeck[_currentIndex];
|
final currentItem = _shuffledDeck[_currentIndex];
|
||||||
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
final correctAnswer = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||||
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
|
? (widget.useKanji && currentItem.kanji != null
|
||||||
|
? currentItem.kanji!
|
||||||
|
: currentItem.characters)
|
||||||
: currentItem.meaning;
|
: currentItem.meaning;
|
||||||
final isCorrect = answer == correctAnswer;
|
final isCorrect = answer == correctAnswer;
|
||||||
|
|
||||||
@@ -157,7 +171,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
|
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
|
||||||
break;
|
break;
|
||||||
case CustomQuizMode.listeningComprehension:
|
case CustomQuizMode.listeningComprehension:
|
||||||
currentItem.srsData.listeningComprehensionNextReview = newNextReview;
|
currentItem.srsData.listeningComprehensionNextReview =
|
||||||
|
newNextReview;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -174,7 +189,8 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
|
currentItem.srsData.englishToJapaneseNextReview = newNextReview;
|
||||||
break;
|
break;
|
||||||
case CustomQuizMode.listeningComprehension:
|
case CustomQuizMode.listeningComprehension:
|
||||||
currentItem.srsData.listeningComprehensionNextReview = newNextReview;
|
currentItem.srsData.listeningComprehensionNextReview =
|
||||||
|
newNextReview;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,35 +210,35 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
widget.onCardReviewed(currentItem);
|
widget.onCardReviewed(currentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SnackBar Logic (new) ---
|
|
||||||
final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
final correctDisplay = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||||
? (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters)
|
? (widget.useKanji && currentItem.kanji != null
|
||||||
|
? currentItem.kanji!
|
||||||
|
: currentItem.characters)
|
||||||
: currentItem.meaning;
|
: currentItem.meaning;
|
||||||
|
|
||||||
final snack = SnackBar(
|
final snack = SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isCorrect ? Colors.greenAccent : Colors.redAccent,
|
color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFF222222),
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
duration: const Duration(milliseconds: 900),
|
duration: const Duration(milliseconds: 900),
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snack);
|
ScaffoldMessenger.of(context).showSnackBar(snack);
|
||||||
}
|
}
|
||||||
// --- End SnackBar Logic ---
|
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
if (widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
if (widget.quizMode == CustomQuizMode.japaneseToEnglish) {
|
||||||
await _speak(currentItem.characters);
|
await _speak(currentItem.characters);
|
||||||
}
|
}
|
||||||
await Future.delayed(const Duration(milliseconds: 500)); // Small delay after correct answer
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
} else {
|
} else {
|
||||||
_shakeController.forward(from: 0);
|
_shakeController.forward(from: 0);
|
||||||
await Future.delayed(const Duration(milliseconds: 900)); // Delay for shake animation
|
await Future.delayed(const Duration(milliseconds: 900));
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextQuestion();
|
_nextQuestion();
|
||||||
@@ -255,15 +271,15 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) {
|
if (_shuffledDeck.isEmpty || _currentIndex >= _shuffledDeck.length) {
|
||||||
return const Center(
|
return Center(child: Text('Review session complete!', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)));
|
||||||
child: Text('Review session complete!'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentItem = _shuffledDeck[_currentIndex];
|
final currentItem = _shuffledDeck[_currentIndex];
|
||||||
final question = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
final question = (widget.quizMode == CustomQuizMode.englishToJapanese)
|
||||||
? currentItem.meaning
|
? currentItem.meaning
|
||||||
: (widget.useKanji && currentItem.kanji != null ? currentItem.kanji! : currentItem.characters);
|
: (widget.useKanji && currentItem.kanji != null
|
||||||
|
? currentItem.kanji!
|
||||||
|
: currentItem.characters);
|
||||||
|
|
||||||
Widget promptWidget;
|
Widget promptWidget;
|
||||||
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
if (widget.quizMode == CustomQuizMode.listeningComprehension) {
|
||||||
@@ -276,7 +292,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
onTap: () => _speak(question),
|
onTap: () => _speak(question),
|
||||||
child: Text(
|
child: Text(
|
||||||
question,
|
question,
|
||||||
style: const TextStyle(fontSize: 48),
|
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -296,10 +312,7 @@ class CustomQuizScreenState extends State<CustomQuizScreen>
|
|||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
minHeight: 150,
|
minHeight: 150,
|
||||||
),
|
),
|
||||||
child: KanjiCard(
|
child: KanjiCard(characterWidget: promptWidget, subtitle: ''),
|
||||||
characterWidget: promptWidget,
|
|
||||||
subtitle: '',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class CustomSrsScreen extends StatefulWidget {
|
|||||||
State<CustomSrsScreen> createState() => _CustomSrsScreenState();
|
State<CustomSrsScreen> createState() => _CustomSrsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProviderStateMixin {
|
class _CustomSrsScreenState extends State<CustomSrsScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
final _deckRepository = CustomDeckRepository();
|
final _deckRepository = CustomDeckRepository();
|
||||||
List<CustomKanjiItem> _deck = [];
|
List<CustomKanjiItem> _deck = [];
|
||||||
@@ -49,7 +50,9 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateCard(CustomKanjiItem item) async {
|
Future<void> _updateCard(CustomKanjiItem item) async {
|
||||||
final index = _deck.indexWhere((element) => element.characters == item.characters);
|
final index = _deck.indexWhere(
|
||||||
|
(element) => element.characters == item.characters,
|
||||||
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_deck[index] = item;
|
_deck[index] = item;
|
||||||
@@ -79,7 +82,8 @@ class _CustomSrsScreenState extends State<CustomSrsScreen> with SingleTickerProv
|
|||||||
item.srsData.listeningComprehensionNextReview!.isBefore(now);
|
item.srsData.listeningComprehensionNextReview!.isBefore(now);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final allDecksEmpty = jpnToEngReviewDeck.isEmpty &&
|
final allDecksEmpty =
|
||||||
|
jpnToEngReviewDeck.isEmpty &&
|
||||||
engToJpnReviewDeck.isEmpty &&
|
engToJpnReviewDeck.isEmpty &&
|
||||||
listeningReviewDeck.isEmpty;
|
listeningReviewDeck.isEmpty;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class HomeScreen extends StatefulWidget {
|
|||||||
State<HomeScreen> createState() => _HomeScreenState();
|
State<HomeScreen> createState() => _HomeScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
|
class _HomeScreenState extends State<HomeScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
List<KanjiItem> _deck = [];
|
List<KanjiItem> _deck = [];
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
@@ -171,8 +172,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
_deck.sort((a, b) {
|
_deck.sort((a, b) {
|
||||||
int getSrsStage(KanjiItem item) {
|
int getSrsStage(KanjiItem item) {
|
||||||
if (mode == QuizMode.reading) {
|
if (mode == QuizMode.reading) {
|
||||||
final onyomiStage = item.srsItems['${QuizMode.reading}onyomi']?.srsStage;
|
final onyomiStage =
|
||||||
final kunyomiStage = item.srsItems['${QuizMode.reading}kunyomi']?.srsStage;
|
item.srsItems['${QuizMode.reading}onyomi']?.srsStage;
|
||||||
|
final kunyomiStage =
|
||||||
|
item.srsItems['${QuizMode.reading}kunyomi']?.srsStage;
|
||||||
|
|
||||||
if (onyomiStage != null && kunyomiStage != null) {
|
if (onyomiStage != null && kunyomiStage != null) {
|
||||||
return min(onyomiStage, kunyomiStage);
|
return min(onyomiStage, kunyomiStage);
|
||||||
@@ -184,8 +187,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
DateTime getLastAsked(KanjiItem item) {
|
DateTime getLastAsked(KanjiItem item) {
|
||||||
if (mode == QuizMode.reading) {
|
if (mode == QuizMode.reading) {
|
||||||
final onyomiLastAsked = item.srsItems['${QuizMode.reading}onyomi']?.lastAsked;
|
final onyomiLastAsked =
|
||||||
final kunyomiLastAsked = item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked;
|
item.srsItems['${QuizMode.reading}onyomi']?.lastAsked;
|
||||||
|
final kunyomiLastAsked =
|
||||||
|
item.srsItems['${QuizMode.reading}kunyomi']?.lastAsked;
|
||||||
|
|
||||||
if (onyomiLastAsked != null && kunyomiLastAsked != null) {
|
if (onyomiLastAsked != null && kunyomiLastAsked != null) {
|
||||||
return onyomiLastAsked.isBefore(kunyomiLastAsked)
|
return onyomiLastAsked.isBefore(kunyomiLastAsked)
|
||||||
@@ -227,16 +232,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
quizState.correctAnswers = [quizState.current!.meanings.first];
|
quizState.correctAnswers = [quizState.current!.meanings.first];
|
||||||
quizState.options = [
|
quizState.options = [
|
||||||
quizState.correctAnswers.first,
|
quizState.correctAnswers.first,
|
||||||
..._dg.generateMeanings(quizState.current!, _deck, 3)
|
..._dg.generateMeanings(quizState.current!, _deck, 3),
|
||||||
].map(_toTitleCase).toList()
|
].map(_toTitleCase).toList()..shuffle();
|
||||||
..shuffle();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuizMode.englishToKanji:
|
case QuizMode.englishToKanji:
|
||||||
quizState.correctAnswers = [quizState.current!.characters];
|
quizState.correctAnswers = [quizState.current!.characters];
|
||||||
quizState.options = [
|
quizState.options = [
|
||||||
quizState.correctAnswers.first,
|
quizState.correctAnswers.first,
|
||||||
..._dg.generateKanji(quizState.current!, _deck, 3)
|
..._dg.generateKanji(quizState.current!, _deck, 3),
|
||||||
]..shuffle();
|
]..shuffle();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -249,16 +253,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
? _deck.expand((k) => k.onyomi)
|
? _deck.expand((k) => k.onyomi)
|
||||||
: _deck.expand((k) => k.kunyomi);
|
: _deck.expand((k) => k.kunyomi);
|
||||||
|
|
||||||
final distractors = readingsSource
|
final distractors =
|
||||||
|
readingsSource
|
||||||
.where((r) => !quizState.correctAnswers.contains(r))
|
.where((r) => !quizState.correctAnswers.contains(r))
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList()
|
.toList()
|
||||||
..shuffle();
|
..shuffle();
|
||||||
quizState.options = ([
|
quizState.options = ([
|
||||||
quizState.correctAnswers[_random.nextInt(quizState.correctAnswers.length)],
|
quizState.correctAnswers[_random.nextInt(
|
||||||
...distractors.take(3)
|
quizState.correctAnswers.length,
|
||||||
])
|
)],
|
||||||
..shuffle();
|
...distractors.take(3),
|
||||||
|
])..shuffle();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,14 +285,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
String readingType = '';
|
String readingType = '';
|
||||||
if (mode == QuizMode.reading) {
|
if (mode == QuizMode.reading) {
|
||||||
readingType = quizState.readingHint.contains("on'yomi") ? 'onyomi' : 'kunyomi';
|
readingType = quizState.readingHint.contains("on'yomi")
|
||||||
|
? 'onyomi'
|
||||||
|
: 'kunyomi';
|
||||||
}
|
}
|
||||||
final srsKey = mode.toString() + readingType;
|
final srsKey = mode.toString() + readingType;
|
||||||
|
|
||||||
var srsItem = current.srsItems[srsKey];
|
var srsItem = current.srsItems[srsKey];
|
||||||
final isNew = srsItem == null;
|
final isNew = srsItem == null;
|
||||||
final srsItemForUpdate = srsItem ??=
|
final srsItemForUpdate = srsItem ??= SrsItem(
|
||||||
SrsItem(kanjiId: current.id, quizMode: mode, readingType: readingType);
|
kanjiId: current.id,
|
||||||
|
quizMode: mode,
|
||||||
|
readingType: readingType,
|
||||||
|
);
|
||||||
|
|
||||||
quizState.asked += 1;
|
quizState.asked += 1;
|
||||||
|
|
||||||
@@ -294,9 +305,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
quizState.showResult = true;
|
quizState.showResult = true;
|
||||||
|
|
||||||
setState(() {}); // Trigger UI rebuild to show selected/correct colors
|
setState(() {});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
quizState.score += 1;
|
quizState.score += 1;
|
||||||
@@ -310,6 +319,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
srsItemForUpdate.lastAsked = DateTime.now();
|
srsItemForUpdate.lastAsked = DateTime.now();
|
||||||
current.srsItems[srsKey] = srsItemForUpdate;
|
current.srsItems[srsKey] = srsItemForUpdate;
|
||||||
|
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
await repo.insertSrsItem(srsItemForUpdate);
|
await repo.insertSrsItem(srsItemForUpdate);
|
||||||
} else {
|
} else {
|
||||||
@@ -326,19 +338,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
content: Text(
|
content: Text(
|
||||||
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isCorrect ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error,
|
color: isCorrect
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.error,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||||
duration: const Duration(milliseconds: 900),
|
duration: const Duration(milliseconds: 900),
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snack);
|
scaffoldMessenger.showSnackBar(snack);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAnswering = true; // Disable input after showing result
|
_isAnswering = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
Future.delayed(const Duration(milliseconds: 900), () {
|
Future.delayed(const Duration(milliseconds: 900), () {
|
||||||
@@ -357,7 +371,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('WaniKani API key is not set.', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
Text(
|
||||||
|
'WaniKani API key is not set.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -389,11 +408,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: [
|
children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)],
|
||||||
_buildQuizPage(0),
|
|
||||||
_buildQuizPage(1),
|
|
||||||
_buildQuizPage(2),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -430,11 +445,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_status,
|
_status,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_loading)
|
if (_loading)
|
||||||
CircularProgressIndicator(color: Theme.of(context).colorScheme.primary),
|
CircularProgressIndicator(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
@@ -472,7 +491,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Score: ${quizState.score} / ${quizState.asked}',
|
'Score: ${quizState.score} / ${quizState.asked}',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hirameki_srs/src/models/theme_model.dart';
|
||||||
|
import 'package:hirameki_srs/src/themes.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../services/deck_repository.dart';
|
import '../services/deck_repository.dart';
|
||||||
@@ -50,24 +52,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
await repo.setApiKey(apiKey);
|
await repo.setApiKey(apiKey);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(content: Text('API key saved!')),
|
context,
|
||||||
);
|
).showSnackBar(const SnackBar(content: Text('API key saved!')));
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(
|
||||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
context,
|
||||||
);
|
).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final themeModel = Provider.of<ThemeModel>(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF121212),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Settings'),
|
title: const Text('Settings'),
|
||||||
backgroundColor: const Color(0xFF1F1F1F),
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@@ -76,15 +80,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: _apiKeyController,
|
controller: _apiKeyController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'WaniKani API Key',
|
labelText: 'WaniKani API Key',
|
||||||
labelStyle: const TextStyle(color: Colors.grey),
|
labelStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF1E1E1E),
|
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
borderSide: const BorderSide(color: Colors.grey),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -92,16 +100,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _saveApiKey,
|
onPressed: _saveApiKey,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blueAccent,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
child: const Text('Save & Start Quiz'),
|
child: const Text('Save & Start Quiz'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Play audio for vocabulary',
|
'Play audio for vocabulary',
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
value: _playAudio,
|
value: _playAudio,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
@@ -111,18 +121,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_playAudio = value;
|
_playAudio = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
activeThumbColor: Colors.blueAccent,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
inactiveThumbColor: Colors.grey,
|
inactiveThumbColor: Theme.of(
|
||||||
tileColor: const Color(0xFF1E1E1E),
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Play sound on correct answer',
|
'Play sound on correct answer',
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
value: _playCorrectSound,
|
value: _playCorrectSound,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
@@ -132,9 +146,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_playCorrectSound = value;
|
_playCorrectSound = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
activeThumbColor: Colors.blueAccent,
|
activeThumbColor: Theme.of(context).colorScheme.primary,
|
||||||
inactiveThumbColor: Colors.grey,
|
inactiveThumbColor: Theme.of(
|
||||||
tileColor: const Color(0xFF1E1E1E),
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
'Theme',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: DropdownButton<ThemeData>(
|
||||||
|
value: themeModel.currentTheme,
|
||||||
|
dropdownColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: Themes.dark,
|
||||||
|
child: const Text('Dark'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: Themes.light,
|
||||||
|
child: const Text('Light'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: Themes.nier,
|
||||||
|
child: const Text('Nier'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (theme) {
|
||||||
|
if (theme != null) {
|
||||||
|
themeModel.setTheme(theme);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class VocabScreen extends StatefulWidget {
|
|||||||
State<VocabScreen> createState() => _VocabScreenState();
|
State<VocabScreen> createState() => _VocabScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStateMixin {
|
class _VocabScreenState extends State<VocabScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
List<VocabularyItem> _deck = [];
|
List<VocabularyItem> _deck = [];
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
@@ -150,7 +151,9 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
|
|
||||||
List<VocabularyItem> currentDeckForMode = _deck;
|
List<VocabularyItem> currentDeckForMode = _deck;
|
||||||
if (mode == VocabQuizMode.audioToEnglish) {
|
if (mode == VocabQuizMode.audioToEnglish) {
|
||||||
currentDeckForMode = _deck.where((item) => item.pronunciationAudios.isNotEmpty).toList();
|
currentDeckForMode = _deck
|
||||||
|
.where((item) => item.pronunciationAudios.isNotEmpty)
|
||||||
|
.toList();
|
||||||
if (currentDeckForMode.isEmpty) {
|
if (currentDeckForMode.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_status = 'No vocabulary with audio found.';
|
_status = 'No vocabulary with audio found.';
|
||||||
@@ -160,13 +163,16 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a new session or we've gone through all shuffled items, re-shuffle
|
if (quizState.shuffledDeck.isEmpty ||
|
||||||
if (quizState.shuffledDeck.isEmpty || quizState.currentIndex >= quizState.shuffledDeck.length) {
|
quizState.currentIndex >= quizState.shuffledDeck.length) {
|
||||||
quizState.shuffledDeck = currentDeckForMode.toList(); // Start with a fresh copy
|
quizState.shuffledDeck = currentDeckForMode.toList();
|
||||||
// Apply sorting based on SRS stages here, but only once per shuffle
|
|
||||||
quizState.shuffledDeck.sort((a, b) {
|
quizState.shuffledDeck.sort((a, b) {
|
||||||
final aSrsItem = a.srsItems[mode.toString()] ?? VocabSrsItem(vocabId: a.id, quizMode: mode);
|
final aSrsItem =
|
||||||
final bSrsItem = b.srsItems[mode.toString()] ?? VocabSrsItem(vocabId: b.id, quizMode: mode);
|
a.srsItems[mode.toString()] ??
|
||||||
|
VocabSrsItem(vocabId: a.id, quizMode: mode);
|
||||||
|
final bSrsItem =
|
||||||
|
b.srsItems[mode.toString()] ??
|
||||||
|
VocabSrsItem(vocabId: b.id, quizMode: mode);
|
||||||
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
|
final stageComparison = aSrsItem.srsStage.compareTo(bSrsItem.srsStage);
|
||||||
if (stageComparison != 0) {
|
if (stageComparison != 0) {
|
||||||
return stageComparison;
|
return stageComparison;
|
||||||
@@ -176,8 +182,8 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
quizState.currentIndex = 0;
|
quizState.currentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
quizState.current = quizState.shuffledDeck[quizState.currentIndex]; // Pick from shuffled deck
|
quizState.current = quizState.shuffledDeck[quizState.currentIndex];
|
||||||
quizState.currentIndex++; // Advance index
|
quizState.currentIndex++;
|
||||||
|
|
||||||
quizState.key = UniqueKey();
|
quizState.key = UniqueKey();
|
||||||
if (mode == VocabQuizMode.audioToEnglish) {
|
if (mode == VocabQuizMode.audioToEnglish) {
|
||||||
@@ -195,16 +201,15 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
quizState.correctAnswers = [quizState.current!.meanings.first];
|
quizState.correctAnswers = [quizState.current!.meanings.first];
|
||||||
quizState.options = [
|
quizState.options = [
|
||||||
quizState.correctAnswers.first,
|
quizState.correctAnswers.first,
|
||||||
..._dg.generateVocabMeanings(quizState.current!, _deck, 3)
|
..._dg.generateVocabMeanings(quizState.current!, _deck, 3),
|
||||||
].map(_toTitleCase).toList()
|
].map(_toTitleCase).toList()..shuffle();
|
||||||
..shuffle();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VocabQuizMode.englishToVocab:
|
case VocabQuizMode.englishToVocab:
|
||||||
quizState.correctAnswers = [quizState.current!.characters];
|
quizState.correctAnswers = [quizState.current!.characters];
|
||||||
quizState.options = [
|
quizState.options = [
|
||||||
quizState.correctAnswers.first,
|
quizState.correctAnswers.first,
|
||||||
..._dg.generateVocab(quizState.current!, _deck, 3)
|
..._dg.generateVocab(quizState.current!, _deck, 3),
|
||||||
]..shuffle();
|
]..shuffle();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -218,14 +223,16 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
final current = _currentQuizState.current;
|
final current = _currentQuizState.current;
|
||||||
if (current == null || current.pronunciationAudios.isEmpty) return;
|
if (current == null || current.pronunciationAudios.isEmpty) return;
|
||||||
|
|
||||||
final maleAudios = current.pronunciationAudios.where((a) => a.gender == 'male');
|
final maleAudios = current.pronunciationAudios.where(
|
||||||
final audioUrl = (maleAudios.isNotEmpty ? maleAudios.first.url : current.pronunciationAudios.first.url);
|
(a) => a.gender == 'male',
|
||||||
|
);
|
||||||
|
final audioUrl = (maleAudios.isNotEmpty
|
||||||
|
? maleAudios.first.url
|
||||||
|
: current.pronunciationAudios.first.url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _audioPlayer.play(UrlSource(audioUrl));
|
await _audioPlayer.play(UrlSource(audioUrl));
|
||||||
} catch (e) {
|
} finally {}
|
||||||
// Ignore player errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _answer(String option) async {
|
void _answer(String option) async {
|
||||||
@@ -248,7 +255,7 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
quizState.asked += 1;
|
quizState.asked += 1;
|
||||||
quizState.selectedOption = option;
|
quizState.selectedOption = option;
|
||||||
quizState.showResult = true;
|
quizState.showResult = true;
|
||||||
setState(() {}); // Trigger UI rebuild to show selected/correct colors
|
setState(() {});
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
quizState.score += 1;
|
quizState.score += 1;
|
||||||
@@ -269,28 +276,28 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
? _toTitleCase(quizState.correctAnswers.first)
|
? _toTitleCase(quizState.correctAnswers.first)
|
||||||
: quizState.correctAnswers.first;
|
: quizState.correctAnswers.first;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
final snack = SnackBar(
|
final snack = SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
isCorrect ? 'Correct!' : 'Wrong — correct: $correctDisplay',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isCorrect ? Colors.greenAccent : Colors.redAccent,
|
color: isCorrect ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.error,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFF222222),
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
duration: const Duration(milliseconds: 900),
|
duration: const Duration(milliseconds: 900),
|
||||||
);
|
);
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snack);
|
ScaffoldMessenger.of(context).showSnackBar(snack);
|
||||||
}
|
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
if (_playCorrectSound) {
|
if (_playCorrectSound) {
|
||||||
await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
|
await _audioPlayer.play(AssetSource('sfx/confirm.mp3'));
|
||||||
}
|
}
|
||||||
if (_playAudio && mode != VocabQuizMode.audioToEnglish) {
|
if (_playAudio && mode != VocabQuizMode.audioToEnglish) {
|
||||||
final maleAudios =
|
final maleAudios = current.pronunciationAudios.where(
|
||||||
current.pronunciationAudios.where((a) => a.gender == 'male');
|
(a) => a.gender == 'male',
|
||||||
|
);
|
||||||
if (maleAudios.isNotEmpty) {
|
if (maleAudios.isNotEmpty) {
|
||||||
final completer = Completer<void>();
|
final completer = Completer<void>();
|
||||||
final sub = _audioPlayer.onPlayerComplete.listen((event) {
|
final sub = _audioPlayer.onPlayerComplete.listen((event) {
|
||||||
@@ -300,19 +307,15 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
try {
|
try {
|
||||||
await _audioPlayer.play(UrlSource(maleAudios.first.url));
|
await _audioPlayer.play(UrlSource(maleAudios.first.url));
|
||||||
await completer.future.timeout(const Duration(seconds: 5));
|
await completer.future.timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
|
||||||
// Ignore player errors
|
|
||||||
} finally {
|
} finally {
|
||||||
await sub.cancel();
|
await sub.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// No fixed delay for incorrect answers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAnswering = true; // Disable input after showing result
|
_isAnswering = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
_nextQuestion();
|
_nextQuestion();
|
||||||
@@ -327,13 +330,14 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text('WaniKani API key is not set.'),
|
Text('WaniKani API key is not set.', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
_loadDeck();
|
_loadDeck();
|
||||||
},
|
},
|
||||||
child: const Text('Go to Settings'),
|
child: const Text('Go to Settings'),
|
||||||
@@ -358,11 +362,7 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
),
|
),
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: [
|
children: [_buildQuizPage(0), _buildQuizPage(1), _buildQuizPage(2)],
|
||||||
_buildQuizPage(0),
|
|
||||||
_buildQuizPage(1),
|
|
||||||
_buildQuizPage(2),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
promptWidget = const SizedBox.shrink();
|
promptWidget = const SizedBox.shrink();
|
||||||
} else if (mode == VocabQuizMode.audioToEnglish) {
|
} else if (mode == VocabQuizMode.audioToEnglish) {
|
||||||
promptWidget = IconButton(
|
promptWidget = IconButton(
|
||||||
icon: const Icon(Icons.volume_up, color: Colors.white, size: 64),
|
icon: Icon(Icons.volume_up, color: Theme.of(context).colorScheme.onSurface, size: 64),
|
||||||
onPressed: _playCurrentAudio,
|
onPressed: _playCurrentAudio,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -390,10 +390,12 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
promptText = _toTitleCase(quizState.current!.meanings.first);
|
promptText = _toTitleCase(quizState.current!.meanings.first);
|
||||||
break;
|
break;
|
||||||
case VocabQuizMode.audioToEnglish:
|
case VocabQuizMode.audioToEnglish:
|
||||||
// Handled above
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
promptWidget = Text(promptText, style: const TextStyle(fontSize: 48, color: Colors.white));
|
promptWidget = Text(
|
||||||
|
promptText,
|
||||||
|
style: TextStyle(fontSize: 48, color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -403,13 +405,9 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: Text(_status, style: TextStyle(color: Theme.of(context).colorScheme.onSurface))),
|
||||||
child: Text(
|
|
||||||
_status,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_loading)
|
if (_loading)
|
||||||
const CircularProgressIndicator(color: Colors.blueAccent),
|
CircularProgressIndicator(color: Theme.of(context).colorScheme.primary),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
@@ -422,10 +420,7 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
minHeight: 150,
|
minHeight: 150,
|
||||||
),
|
),
|
||||||
child: KanjiCard(
|
child: KanjiCard(characterWidget: promptWidget, subtitle: ''),
|
||||||
characterWidget: promptWidget,
|
|
||||||
subtitle: '',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -445,7 +440,7 @@ class _VocabScreenState extends State<VocabScreen> with SingleTickerProviderStat
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Score: ${quizState.score} / ${quizState.asked}',
|
'Score: ${quizState.score} / ${quizState.asked}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -103,8 +103,6 @@ class DeckRepository {
|
|||||||
try {
|
try {
|
||||||
envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// dotenv is not initialized, so we can't get the key.
|
|
||||||
// This is expected in release builds.
|
|
||||||
envApiKey = null;
|
envApiKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +259,4 @@ class DeckRepository {
|
|||||||
await saveKanji(items);
|
await saveKanji(items);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -104,8 +104,6 @@ class VocabDeckRepository {
|
|||||||
try {
|
try {
|
||||||
envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
envApiKey = dotenv.env['WANIKANI_API_KEY'];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// dotenv is not initialized, so we can't get the key.
|
|
||||||
// This is expected in release builds.
|
|
||||||
envApiKey = null;
|
envApiKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +203,7 @@ class VocabDeckRepository {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} finally {}
|
||||||
// Error decoding, so we'll just have no audio for this item
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return VocabularyItem(
|
return VocabularyItem(
|
||||||
id: r['id'] as int,
|
id: r['id'] as int,
|
||||||
|
|||||||
129
lib/src/themes.dart
Normal file
129
lib/src/themes.dart
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SrsColors {
|
||||||
|
final Color level1;
|
||||||
|
final Color level2;
|
||||||
|
final Color level3;
|
||||||
|
final Color level4;
|
||||||
|
final Color level5;
|
||||||
|
final Color level6;
|
||||||
|
final Color level7;
|
||||||
|
final Color level8;
|
||||||
|
final Color level9;
|
||||||
|
|
||||||
|
const SrsColors({
|
||||||
|
required this.level1,
|
||||||
|
required this.level2,
|
||||||
|
required this.level3,
|
||||||
|
required this.level4,
|
||||||
|
required this.level5,
|
||||||
|
required this.level6,
|
||||||
|
required this.level7,
|
||||||
|
required this.level8,
|
||||||
|
required this.level9,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CustomTheme on ThemeData {
|
||||||
|
SrsColors get srsColors {
|
||||||
|
if (brightness == Brightness.dark) {
|
||||||
|
return const SrsColors(
|
||||||
|
level1: Color(0xFFE57373), // red
|
||||||
|
level2: Color(0xFFFFB74D), // orange
|
||||||
|
level3: Color(0xFFFFD54F), // yellow
|
||||||
|
level4: Color(0xFFDCE775), // lime
|
||||||
|
level5: Color(0xFFAED581), // light green
|
||||||
|
level6: Color(0xFF81C784), // green
|
||||||
|
level7: Color(0xFF4DB6AC), // teal
|
||||||
|
level8: Color(0xFF4FC3F7), // light blue
|
||||||
|
level9: Color(0xFF7986CB), // indigo
|
||||||
|
);
|
||||||
|
} else if (colorScheme.primary == const Color(0xFF7B6D53)) { // Nier theme
|
||||||
|
return const SrsColors(
|
||||||
|
level1: Color(0xFFB71C1C), // dark red
|
||||||
|
level2: Color(0xFFD84315), // deep orange
|
||||||
|
level3: Color(0xFFF57F17), // yellow
|
||||||
|
level4: Color(0xFF9E9D24), // lime
|
||||||
|
level5: Color(0xFF558B2F), // light green
|
||||||
|
level6: Color(0xFF2E7D32), // green
|
||||||
|
level7: Color(0xFF00695C), // teal
|
||||||
|
level8: Color(0xFF0277BD), // light blue
|
||||||
|
level9: Color(0xFF283593), // indigo
|
||||||
|
);
|
||||||
|
} else { // Light theme
|
||||||
|
return const SrsColors(
|
||||||
|
level1: Colors.red,
|
||||||
|
level2: Colors.orange,
|
||||||
|
level3: Colors.yellow,
|
||||||
|
level4: Colors.lightGreen,
|
||||||
|
level5: Colors.green,
|
||||||
|
level6: Colors.teal,
|
||||||
|
level7: Colors.cyan,
|
||||||
|
level8: Colors.blue,
|
||||||
|
level9: Colors.purple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Themes {
|
||||||
|
static final dark = ThemeData(
|
||||||
|
colorScheme: const ColorScheme(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primary: Color(0xFF90CAF9),
|
||||||
|
onPrimary: Colors.black,
|
||||||
|
secondary: Color(0xFFBBDEFB),
|
||||||
|
onSecondary: Colors.black,
|
||||||
|
tertiary: Color(0xFFA5D6A7),
|
||||||
|
onTertiary: Colors.black,
|
||||||
|
error: Color(0xFFEF9A9A),
|
||||||
|
onError: Colors.black,
|
||||||
|
surface: Color(0xFF121212),
|
||||||
|
onSurface: Colors.white,
|
||||||
|
surfaceContainer: Color(0xFF1E1E1E),
|
||||||
|
surfaceContainerHighest: Color(0xFF424242),
|
||||||
|
onSurfaceVariant: Colors.white70,
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final light = ThemeData(
|
||||||
|
colorScheme: const ColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: Color(0xFF1976D2),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
secondary: Color(0xFF42A5F5),
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
tertiary: Color(0xFF66BB6A),
|
||||||
|
onTertiary: Colors.white,
|
||||||
|
error: Color(0xFFE57373),
|
||||||
|
onError: Colors.white,
|
||||||
|
surface: Color(0xFFFFFFFF),
|
||||||
|
onSurface: Colors.black,
|
||||||
|
surfaceContainer: Color(0xFFF5F5F5),
|
||||||
|
surfaceContainerHighest: Color(0xFFE0E0E0),
|
||||||
|
onSurfaceVariant: Colors.black54,
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final nier = ThemeData(
|
||||||
|
colorScheme: const ColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: Color(0xFF7B6D53),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
secondary: Color(0xFFA99A7E),
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
tertiary: Color(0xFFA99A7E),
|
||||||
|
onTertiary: Colors.white,
|
||||||
|
error: Color(0xFFD32F2F),
|
||||||
|
onError: Colors.white,
|
||||||
|
surface: Color(0xFFCFCBAA),
|
||||||
|
onSurface: Color(0xFF333333),
|
||||||
|
surfaceContainer: Color(0xFFBDB898),
|
||||||
|
surfaceContainerHighest: Color(0xFFA8A388),
|
||||||
|
onSurfaceVariant: Color(0xFF545454),
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user