import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:ju_learn/learn.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() => runApp(const LearnApp()); class LearnApp extends StatelessWidget { const LearnApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Learn App', theme: ThemeData(useMaterial3: true, brightness: Brightness.light), darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark), home: const MainPage(), ); } } class MainPage extends StatefulWidget { const MainPage({super.key}); @override MainPageState createState() => MainPageState(); } class MainPageState extends State { List _vaults = []; MainPageState() { loadList(); } Vault? getByName(String name) { for (var v in _vaults) { if (v.name == name) return v; } return null; } loadList() async { final prefs = await SharedPreferences.getInstance(); List? json = prefs.getStringList("vaults"); List? jsonState = prefs.getStringList("vaultStates"); if (json == null) return; setState(() { _vaults = json.map((e) => Vault.fromJson(jsonDecode(e))).toList(); }); if (jsonState == null) return; for (var s in jsonState) { var sMap = jsonDecode(s); Vault? v = getByName(sMap["name"]); if (v == null) continue; v.loadState(sMap); } } saveList() async { final prefs = await SharedPreferences.getInstance(); prefs.setStringList( "vaults", _vaults.map((e) => jsonEncode(e.toJson())).toList()); } saveStateList() async { final prefs = await SharedPreferences.getInstance(); prefs.setStringList("vaultStates", _vaults.map((e) => jsonEncode(e.toStateJson())).toList()); } _vaultMenu(Vault v) async { showModalBottomSheet( context: context, builder: (context) => BottomSheet( builder: (context) => Column(children: [ Padding( padding: const EdgeInsets.all(8), child: Column(children: [ Text(v.name, style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, )), Text('Stats: ${v.txtVals()}', style: const TextStyle( fontSize: 10.0, fontWeight: FontWeight.bold, )) ])), ListTile( leading: const Icon(Icons.restore), title: const Text("Reset Vault"), subtitle: const Text("Reset Vault learn Data to default."), trailing: const Icon(Icons.chevron_right), onTap: () { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Reset Vault'), content: Text('Do you want to reset „${v.name}“ Vault?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Cancel')), FilledButton( child: const Text('Reset'), onPressed: () { setState(() { v.reset(); }); saveStateList(); Navigator.of(context).pop(); Navigator.of(context).pop(); }, ), ], ); }, ); }, ), ListTile( leading: const Icon(Icons.delete), title: const Text("Vault löschen"), subtitle: Text('Do you want to delete „${v.name}“ Vault?'), trailing: const Icon(Icons.chevron_right), onTap: () async { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Delete Vault'), content: Text( 'Do you want to delete „${v.name}“ Vault?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Cancel')), FilledButton( child: const Text('Delete'), onPressed: () { setState(() { _vaults.remove(v); }); saveList(); saveStateList(); Navigator.of(context).pop(); Navigator.of(context).pop(); }, ), ], ); }, ); }) ]), onClosing: () {}, )); } _pickFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['json'], allowMultiple: false); if (result != null) { String data = ""; if (!kIsWeb && result.files.single.path != null) { File file = File(result.files.single.path ?? ""); data = file.readAsStringSync(); } else { PlatformFile pfile = result.files.first; data = String.fromCharCodes(pfile.bytes?.map((e) => e) ?? []); } try { if (data.isEmpty) throw ErrorDescription("Empty File!"); Vault v = Vault.fromJson(jsonDecode(data)); setState(() { _vaults.add(v); }); saveList(); } catch (e) { print(e); AlertDialog( title: const Text('Error'), content: const Text('An error has occurred.'), actions: [ TextButton( child: const Text('OK'), onPressed: () { Navigator.of(context).pop(); }, ), ]); return; } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Learn App'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ..._vaults.map((e) => Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => QuizPage(e, saveStateList)), ); }, onLongPress: () { _vaultMenu(e); }, child: Text(e.name), ))) ]), ), floatingActionButton: FloatingActionButton( onPressed: _pickFile, child: const Icon(Icons.add), ), ); } } class Question { String quest; List answers; int correct; String explanation; int drawer = 0; int lastInRun = 0; Question(this.quest, this.answers, this.correct, this.explanation); Map toJson() { return { "quest": quest, "answers": answers, "correct": correct, "explanation": explanation, }; } factory Question.fromJson(Map json) { if (json["quest"] is! String) { throw ErrorDescription("fromJSON Error: quest"); } if (json["answers"] is! List) { throw ErrorDescription("fromJSON Error: awnsers"); } if (json["correct"] is! int || json["correct"] < 0 || json["correct"] >= json["answers"].length) { throw ErrorDescription("fromJSON Error: correct"); } if (json["explanation"] is! String) { throw ErrorDescription("fromJSON Error: explanation"); } return Question( json["quest"], (json["answers"] as List).map((d) => d.toString()).toList(), json["correct"], json["explanation"], ); } } class Vault { String name; List questions; Vault(this.name, this.questions); Map toJson() { return { "name": name, "questions": questions.map((e) => e.toJson()).toList() }; } Map toStateJson() { return {"name": name, "drawers": questions.map((d) => d.drawer).toList()}; } void reset() { for (var q in questions) { q.drawer = 0; q.lastInRun = 0; } } void softReset() { for (var q in questions) { q.lastInRun = 0; } } (int, double, int) vals() { int min = 0xffffff; int total = 0; int max = 0; for (var q in questions) { if (q.drawer > max) max = q.drawer; if (q.drawer < min) min = q.drawer; total += q.drawer; } return (min, total.toDouble() / questions.length, max); } String txtVals() { var (min, avg, max) = vals(); return '($min, ${(avg * 100).round() / 100.0}, $max)'; } factory Vault.fromJson(Map json) { if (json["name"] is! String) { throw ErrorDescription("fromJSON Error: name"); } if (json["questions"] is! List) { throw ErrorDescription("fromJSON Error: questions"); } return Vault( json["name"], (json["questions"] as List) .map((d) => Question.fromJson(d)) .toList(), ); } loadState(Map json) { if (json["name"] != name) throw ErrorDescription("Wrong Vault Error"); if (json["drawers"] is! List) { throw ErrorDescription("fromJSON Error: questions"); } var stateList = json["drawers"]; for (var i = 0; i < stateList.length; i++) { var q = questions.elementAtOrNull(i); if (q != null) { q.drawer = stateList[i] as int; } } } }