diff --git a/README.md b/README.md index 117db86..c7e5ae9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ### Clone the project ```sh -git clone --recursive https://github.com/refilc/naplo +git clone https://github.com/refilc/naplo cd naplo ``` diff --git a/filcnaplo/assets/images/subject_covers/math_light.png b/filcnaplo/assets/images/subject_covers/math_light.png new file mode 100644 index 0000000..eb3e2f5 Binary files /dev/null and b/filcnaplo/assets/images/subject_covers/math_light.png differ diff --git a/filcnaplo/ios/Runner.xcodeproj/project.pbxproj b/filcnaplo/ios/Runner.xcodeproj/project.pbxproj index ec6cc14..482cacc 100644 --- a/filcnaplo/ios/Runner.xcodeproj/project.pbxproj +++ b/filcnaplo/ios/Runner.xcodeproj/project.pbxproj @@ -198,9 +198,9 @@ isa = PBXNativeTarget; buildConfigurationList = 3127F79F28EAEDE300C2EFB3 /* Build configuration list for PBXNativeTarget "livecard" */; buildPhases = ( + 3127F78A28EAEDE200C2EFB3 /* Resources */, 3127F78828EAEDE200C2EFB3 /* Sources */, 3127F78928EAEDE200C2EFB3 /* Frameworks */, - 3127F78A28EAEDE200C2EFB3 /* Resources */, ); buildRules = ( ); diff --git a/filcnaplo/lib/app.dart b/filcnaplo/lib/app.dart index 581b4a0..15a410c 100644 --- a/filcnaplo/lib/app.dart +++ b/filcnaplo/lib/app.dart @@ -114,7 +114,8 @@ class App extends StatelessWidget { ChangeNotifierProvider( create: (context) => ExamProvider(context: context)), ChangeNotifierProvider( - create: (context) => HomeworkProvider(context: context)), + create: (context) => + HomeworkProvider(context: context, database: database)), ChangeNotifierProvider( create: (context) => MessageProvider(context: context)), ChangeNotifierProvider( diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 6435392..7c29324 100644 --- a/filcnaplo/lib/database/init.dart +++ b/filcnaplo/lib/database/init.dart @@ -47,6 +47,11 @@ const userDataDB = DatabaseStruct("user_data", { "renamed_teachers": String, // "subject_lesson_count": String, // non kreta data "last_seen_grade": int, + // goal planning // non kreta data + "goal_plans": String, + "goal_averages": String, + "goal_befores": String, + "goal_pin_dates": String, }); Future createTable(Database db, DatabaseStruct struct) => @@ -98,6 +103,11 @@ Future initDB(DatabaseProvider database) async { "renamed_teachers": "{}", // "subject_lesson_count": "{}", // non kreta data "last_seen_grade": 0, + // goal planning // non kreta data + "goal_plans": "{}", + "goal_averages": "{}", + "goal_befores": "{}", + "goal_pin_dates": "{}", }); } catch (error) { print("ERROR: migrateDB: $error"); diff --git a/filcnaplo/lib/database/query.dart b/filcnaplo/lib/database/query.dart index fb892f6..fb3094c 100644 --- a/filcnaplo/lib/database/query.dart +++ b/filcnaplo/lib/database/query.dart @@ -192,6 +192,7 @@ class UserDatabaseQuery { return lastSeen; } + // renamed things Future> renamedSubjects({required String userId}) async { List userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); @@ -213,4 +214,49 @@ class UserDatabaseQuery { return (jsonDecode(renamedTeachersJson) as Map) .map((key, value) => MapEntry(key.toString(), value.toString())); } + + // goal planner + Future> subjectGoalPlans({required String userId}) async { + List userData = + await db.query("user_data", where: "id = ?", whereArgs: [userId]); + if (userData.isEmpty) return {}; + String? goalPlansJson = userData.elementAt(0)["goal_plans"] as String?; + if (goalPlansJson == null) return {}; + return (jsonDecode(goalPlansJson) as Map) + .map((key, value) => MapEntry(key.toString(), value.toString())); + } + + Future> subjectGoalAverages( + {required String userId}) async { + List userData = + await db.query("user_data", where: "id = ?", whereArgs: [userId]); + if (userData.isEmpty) return {}; + String? goalAvgsJson = userData.elementAt(0)["goal_averages"] as String?; + if (goalAvgsJson == null) return {}; + return (jsonDecode(goalAvgsJson) as Map) + .map((key, value) => MapEntry(key.toString(), value.toString())); + } + + Future> subjectGoalBefores( + {required String userId}) async { + List userData = + await db.query("user_data", where: "id = ?", whereArgs: [userId]); + if (userData.isEmpty) return {}; + String? goalBeforesJson = userData.elementAt(0)["goal_befores"] as String?; + if (goalBeforesJson == null) return {}; + return (jsonDecode(goalBeforesJson) as Map) + .map((key, value) => MapEntry(key.toString(), value.toString())); + } + + Future> subjectGoalPinDates( + {required String userId}) async { + List userData = + await db.query("user_data", where: "id = ?", whereArgs: [userId]); + if (userData.isEmpty) return {}; + String? goalPinDatesJson = + userData.elementAt(0)["goal_pin_dates"] as String?; + if (goalPinDatesJson == null) return {}; + return (jsonDecode(goalPinDatesJson) as Map) + .map((key, value) => MapEntry(key.toString(), value.toString())); + } } diff --git a/filcnaplo/lib/database/store.dart b/filcnaplo/lib/database/store.dart index 86330b4..185462d 100644 --- a/filcnaplo/lib/database/store.dart +++ b/filcnaplo/lib/database/store.dart @@ -127,6 +127,7 @@ class UserDatabaseStore { where: "id = ?", whereArgs: [userId]); } + // renamed things Future storeRenamedSubjects(Map subjects, {required String userId}) async { String renamedSubjectsJson = jsonEncode(subjects); @@ -140,4 +141,33 @@ class UserDatabaseStore { await db.update("user_data", {"renamed_teachers": renamedTeachersJson}, where: "id = ?", whereArgs: [userId]); } + + // goal planner + Future storeSubjectGoalPlans(Map plans, + {required String userId}) async { + String goalPlansJson = jsonEncode(plans); + await db.update("user_data", {"goal_plans": goalPlansJson}, + where: "id = ?", whereArgs: [userId]); + } + + Future storeSubjectGoalAverages(Map avgs, + {required String userId}) async { + String goalAvgsJson = jsonEncode(avgs); + await db.update("user_data", {"goal_averages": goalAvgsJson}, + where: "id = ?", whereArgs: [userId]); + } + + Future storeSubjectGoalBefores(Map befores, + {required String userId}) async { + String goalBeforesJson = jsonEncode(befores); + await db.update("user_data", {"goal_befores": goalBeforesJson}, + where: "id = ?", whereArgs: [userId]); + } + + Future storeSubjectGoalPinDates(Map dates, + {required String userId}) async { + String goalPinDatesJson = jsonEncode(dates); + await db.update("user_data", {"goal_pin_dates": goalPinDatesJson}, + where: "id = ?", whereArgs: [userId]); + } } diff --git a/filcnaplo/pubspec.yaml b/filcnaplo/pubspec.yaml index be6afaa..751aa3e 100644 --- a/filcnaplo/pubspec.yaml +++ b/filcnaplo/pubspec.yaml @@ -86,6 +86,7 @@ flutter: - assets/icons/ic_splash.png - assets/animations/ - assets/images/ + - assets/images/subject_covers/ fonts: - family: FilcIcons diff --git a/filcnaplo_kreta_api/lib/providers/homework_provider.dart b/filcnaplo_kreta_api/lib/providers/homework_provider.dart index 8f5fa4d..3a20e1f 100644 --- a/filcnaplo_kreta_api/lib/providers/homework_provider.dart +++ b/filcnaplo_kreta_api/lib/providers/homework_provider.dart @@ -22,9 +22,11 @@ class HomeworkProvider with ChangeNotifier { HomeworkProvider({ List initialHomework = const [], required BuildContext context, + required DatabaseProvider database, }) { _homework = List.castFrom(initialHomework); _context = context; + _database = database; if (_homework.isEmpty) restore(); } diff --git a/filcnaplo_mobile_ui/lib/common/progress_bar.dart b/filcnaplo_mobile_ui/lib/common/progress_bar.dart index a89ce68..ffffa00 100755 --- a/filcnaplo_mobile_ui/lib/common/progress_bar.dart +++ b/filcnaplo_mobile_ui/lib/common/progress_bar.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; class ProgressBar extends StatelessWidget { - const ProgressBar({Key? key, required this.value, this.backgroundColor}) : super(key: key); + const ProgressBar( + {Key? key, required this.value, this.backgroundColor, this.height = 8.0}) + : super(key: key); final double value; final Color? backgroundColor; + final double height; @override Widget build(BuildContext context) { @@ -13,11 +16,13 @@ class ProgressBar extends StatelessWidget { // Background Container( decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.black.withOpacity(0.1) : Colors.white.withOpacity(0.1), + color: Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(0.1) + : Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(45.0), ), width: double.infinity, - height: 8.0, + height: height, ), // Slider @@ -26,8 +31,9 @@ class ProgressBar extends StatelessWidget { width: double.infinity, child: CustomPaint( painter: ProgressPainter( - backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.secondary, - height: 8.0, + backgroundColor: + backgroundColor ?? Theme.of(context).colorScheme.secondary, + height: height, value: value.clamp(0, 1), ), ), @@ -38,7 +44,10 @@ class ProgressBar extends StatelessWidget { } class ProgressPainter extends CustomPainter { - ProgressPainter({required this.height, required this.value, required this.backgroundColor}); + ProgressPainter( + {required this.height, + required this.value, + required this.backgroundColor}); final double height; final double value; @@ -64,6 +73,8 @@ class ProgressPainter extends CustomPainter { @override bool shouldRepaint(ProgressPainter oldDelegate) { - return value != oldDelegate.value || height != oldDelegate.height || backgroundColor != oldDelegate.backgroundColor; + return value != oldDelegate.value || + height != oldDelegate.height || + backgroundColor != oldDelegate.backgroundColor; } } diff --git a/filcnaplo_mobile_ui/lib/common/round_border_icon.dart b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart new file mode 100644 index 0000000..ac1fdf4 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class RoundBorderIcon extends StatelessWidget { + final Color color; + final double width; + final double padding; + final Widget icon; + + const RoundBorderIcon( + {Key? key, + this.color = Colors.black, + this.width = 1.5, + this.padding = 5.0, + required this.icon}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: color, width: width), + borderRadius: BorderRadius.circular(50.0), + ), + child: Padding( + padding: EdgeInsets.all(padding), + child: icon, + ), + ); + } +} diff --git a/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart b/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart index 5b87c8e..0c0c152 100755 --- a/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart +++ b/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:animations/animations.dart'; +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/utils/format.dart'; import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; @@ -21,9 +23,10 @@ import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_pro import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart'; -import 'package:filcnaplo_premium/ui/mobile/goal_planner/test.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.dart'; import 'package:filcnaplo_premium/models/premium_scopes.dart'; import 'package:filcnaplo_premium/providers/premium_provider.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_state_screen.dart'; import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -62,12 +65,16 @@ class _GradeSubjectViewState extends State { late GradeProvider gradeProvider; late GradeCalculatorProvider calculatorProvider; late SettingsProvider settingsProvider; + late DatabaseProvider dbProvider; + late UserProvider user; late double average; late Widget gradeGraph; bool gradeCalcMode = false; + String plan = ''; + List getSubjectGrades(Subject subject) => !gradeCalcMode ? gradeProvider.grades.where((e) => e.subject == subject).toList() : calculatorProvider.grades.where((e) => e.subject == subject).toList(); @@ -151,6 +158,20 @@ class _GradeSubjectViewState extends State { gradeTiles = List.castFrom(tiles); } + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + dbProvider = Provider.of(context, listen: false); + } + + void fetchGoalPlans() async { + plan = (await dbProvider.userQuery + .subjectGoalPlans(userId: user.id!))[widget.subject.id] ?? + ''; + setState(() {}); + } + @override Widget build(BuildContext context) { gradeProvider = Provider.of(context); @@ -196,6 +217,8 @@ class _GradeSubjectViewState extends State { buildTiles(ghostGrades); } + fetchGoalPlans(); + return Scaffold( key: _scaffoldKey, floatingActionButtonLocation: ExpandableFab.location, @@ -213,6 +236,7 @@ class _GradeSubjectViewState extends State { ), children: [ FloatingActionButton.small( + heroTag: "btn_ghost_grades", child: const Icon(FeatherIcons.plus), backgroundColor: Theme.of(context).colorScheme.secondary, onPressed: () { @@ -220,6 +244,7 @@ class _GradeSubjectViewState extends State { }, ), FloatingActionButton.small( + heroTag: "btn_goal_planner", child: const Icon(FeatherIcons.flag, size: 20.0), backgroundColor: Theme.of(context).colorScheme.secondary, onPressed: () { @@ -235,7 +260,7 @@ class _GradeSubjectViewState extends State { Navigator.of(context).push(CupertinoPageRoute( builder: (context) => - GoalPlannerTest(subject: widget.subject))); + GoalPlannerScreen(subject: widget.subject))); }, ), ], @@ -261,6 +286,34 @@ class _GradeSubjectViewState extends State { const SizedBox(width: 6.0), if (average != 0) Center(child: AverageDisplay(average: average)), + const SizedBox(width: 6.0), + if (plan != '') + Center( + child: GestureDetector( + onTap: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => + GoalStateScreen(subject: widget.subject))); + }, + child: Container( + width: 54.0, + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(45.0), + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(.15), + ), + child: Icon( + FeatherIcons.flag, + size: 17.0, + weight: 2.5, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), const SizedBox(width: 12.0), ], icon: SubjectIcon.resolveVariant( diff --git a/filcnaplo_mobile_ui/pubspec.yaml b/filcnaplo_mobile_ui/pubspec.yaml old mode 100755 new mode 100644 index adcf153..498f473 --- a/filcnaplo_mobile_ui/pubspec.yaml +++ b/filcnaplo_mobile_ui/pubspec.yaml @@ -1,54 +1,54 @@ -name: filcnaplo_mobile_ui -publish_to: "none" - -environment: - sdk: ">=2.17.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.2 - - # Filcnaplo main dep - filcnaplo: - path: ../filcnaplo/ - filcnaplo_kreta_api: - path: ../filcnaplo_kreta_api/ - filcnaplo_premium: - path: ../filcnaplo_premium/ - - flutter_feather_icons: ^2.0.0+1 - provider: ^5.0.0 - fl_chart: ^0.45.1 - url_launcher: ^6.0.9 - flutter_material_color_picker: ^1.1.0+2 - photo_view: ^0.14.0 - flutter_linkify: ^5.0.2 - flutter_custom_tabs: ^1.0.3 - flutter_markdown: ^0.6.5 - animations: ^2.0.1 - animated_list_plus: ^0.5.0 - confetti: ^0.6.0 - live_activities: ^1.0.0 - animated_flip_counter: ^0.2.5 - lottie: ^1.4.3 - rive: ^0.9.1 - animated_background: ^2.0.0 - home_widget: ^0.1.6 - dropdown_button2: ^1.8.9 - flutter_svg: ^1.1.6 - background_fetch: ^1.1.5 - wtf_sliding_sheet: ^1.0.0 - package_info_plus: ^4.0.2 - dotted_border: ^2.0.0+3 - screenshot: ^2.1.0 - image_gallery_saver: ^2.0.2 - rounded_expansion_tile: - git: - url: https://github.com/kimaah/rounded_expansion_tile.git - -dev_dependencies: - flutter_lints: ^1.0.0 - -flutter: - uses-material-design: true +name: filcnaplo_mobile_ui +publish_to: "none" + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + + # Filcnaplo main dep + filcnaplo: + path: ../filcnaplo/ + filcnaplo_kreta_api: + path: ../filcnaplo_kreta_api/ + filcnaplo_premium: + path: ../filcnaplo_premium/ + + flutter_feather_icons: ^2.0.0+1 + provider: ^5.0.0 + fl_chart: ^0.45.1 + url_launcher: ^6.0.9 + flutter_material_color_picker: ^1.1.0+2 + photo_view: ^0.14.0 + flutter_linkify: ^5.0.2 + flutter_custom_tabs: ^1.0.3 + flutter_markdown: ^0.6.5 + animations: ^2.0.1 + animated_list_plus: ^0.5.0 + confetti: ^0.6.0 + live_activities: ^1.0.0 + animated_flip_counter: ^0.2.5 + lottie: ^1.4.3 + rive: ^0.9.1 + animated_background: ^2.0.0 + home_widget: ^0.1.6 + dropdown_button2: ^1.8.9 + flutter_svg: ^1.1.6 + background_fetch: ^1.1.5 + wtf_sliding_sheet: ^1.0.0 + package_info_plus: ^4.0.2 + dotted_border: ^2.0.0+3 + screenshot: ^2.1.0 + image_gallery_saver: ^2.0.2 + rounded_expansion_tile: + git: + url: https://github.com/kimaah/rounded_expansion_tile.git + +dev_dependencies: + flutter_lints: ^1.0.0 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart index 52b4a93..22878a9 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart @@ -138,6 +138,14 @@ class Plan { Plan(this.plan); + String get dbString { + var finalString = ''; + for (var i in plan) { + finalString += "$i,"; + } + return finalString; + } + @override bool operator ==(other) => other is Plan && listEquals(plan, other.plan); diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart new file mode 100644 index 0000000..4cf8acf --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart @@ -0,0 +1,414 @@ +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/helpers/average_helper.dart'; +import 'package:filcnaplo/helpers/subject.dart'; +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_kreta_api/models/group_average.dart'; +import 'package:filcnaplo_kreta_api/models/subject.dart'; +import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; +import 'package:filcnaplo_mobile_ui/common/average_display.dart'; +import 'package:filcnaplo_mobile_ui/common/round_border_icon.dart'; +import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +enum PlanResult { + available, // There are possible solutions + unreachable, // The solutions are too hard don't even try + unsolvable, // There are no solutions + reached, // Goal already reached +} + +class GoalPlannerScreen extends StatefulWidget { + final Subject subject; + + const GoalPlannerScreen({Key? key, required this.subject}) : super(key: key); + + @override + State createState() => _GoalPlannerScreenState(); +} + +class _GoalPlannerScreenState extends State { + late GradeProvider gradeProvider; + late GradeCalculatorProvider calculatorProvider; + late SettingsProvider settingsProvider; + late DatabaseProvider dbProvider; + late UserProvider user; + + bool gradeCalcMode = false; + + List getSubjectGrades(Subject subject) => !gradeCalcMode + ? gradeProvider.grades.where((e) => e.subject == subject).toList() + : calculatorProvider.grades.where((e) => e.subject == subject).toList(); + + double goalValue = 4.0; + List grades = []; + + Plan? recommended; + Plan? fastest; + Plan? selectedRoute; + List otherPlans = []; + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + dbProvider = Provider.of(context, listen: false); + } + + Future> fetchGoalPlans() async { + return await dbProvider.userQuery.subjectGoalPlans(userId: user.id!); + } + + Future> fetchGoalAverages() async { + return await dbProvider.userQuery.subjectGoalAverages(userId: user.id!); + } + + // haha bees lol + Future> fetchGoalBees() async { + return await dbProvider.userQuery.subjectGoalBefores(userId: user.id!); + } + + PlanResult getResult() { + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + recommended = null; + fastest = null; + otherPlans = []; + + if (currentAvg >= goalValue) return PlanResult.reached; + + final planner = GoalPlanner(goalValue, grades); + final plans = planner.solve(); + + plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) + .abs() + .compareTo(b.avg - (2 * goalValue + 5) / 3)); + + try { + final singleSolution = plans.every((e) => e.sigma == 0); + recommended = + plans.where((e) => singleSolution ? true : e.sigma > 0).first; + plans.removeWhere((e) => e == recommended); + } catch (_) {} + + plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); + + try { + fastest = plans.removeAt(0); + } catch (_) {} + + if ((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0) >= 3) { + recommended = fastest; + } + + if (recommended == null) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unsolvable; + } + + if (recommended!.plan.length > 10) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unreachable; + } + + otherPlans = List.from(plans); + + return PlanResult.available; + } + + void getGrades() { + grades = getSubjectGrades(widget.subject).toList(); + } + + @override + Widget build(BuildContext context) { + gradeProvider = Provider.of(context); + calculatorProvider = Provider.of(context); + settingsProvider = Provider.of(context); + + getGrades(); + + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + final result = getResult(); + + List subjectGrades = getSubjectGrades(widget.subject); + + double avg = AverageHelper.averageEvals(subjectGrades); + + var nullavg = GroupAverage(average: 0.0, subject: widget.subject, uid: "0"); + double groupAverage = gradeProvider.groupAverages + .firstWhere((e) => e.subject == widget.subject, orElse: () => nullavg) + .average; + + return Scaffold( + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only( + top: 5.0, + bottom: 220.0, + right: 15.0, + left: 2.0, + ), + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // const BackButton(), + // Padding( + // padding: const EdgeInsets.only(right: 15.0), + // child: Row( + // children: [ + // Text( + // 'goal_planner_title'.i18n, + // style: const TextStyle( + // fontWeight: FontWeight.w500, fontSize: 18.0), + // ), + // const SizedBox( + // width: 5, + // ), + // const BetaChip(), + // ], + // ), + // ), + // ], + // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + const BackButton(), + RoundBorderIcon( + icon: Icon( + SubjectIcon.resolveVariant( + context: context, + subject: widget.subject, + ), + size: 18, + weight: 1.5, + ), + ), + const SizedBox( + width: 5.0, + ), + Text( + (widget.subject.isRenamed + ? widget.subject.renamedTo + : widget.subject.name) ?? + 'goal_planner_title'.i18n, + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + Row( + children: [ + if (groupAverage != 0) + AverageDisplay(average: groupAverage, border: true), + const SizedBox(width: 6.0), + AverageDisplay(average: avg) + ], + ), + ], + ), + const SizedBox(height: 12.0), + Padding( + padding: const EdgeInsets.only(left: 22.0, right: 22.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "set_a_goal".i18n, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + ), + const SizedBox(height: 4.0), + Text( + goalValue.toString(), + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 48.0, + color: gradeColor(goalValue.round(), settingsProvider), + ), + ), + // Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text( + // "select_subject".i18n, + // style: const TextStyle( + // fontWeight: FontWeight.bold, + // fontSize: 20.0, + // ), + // ), + // const SizedBox(height: 4.0), + // Column( + // children: [ + // Icon( + // SubjectIcon.resolveVariant( + // context: context, + // subject: widget.subject, + // ), + // size: 48.0, + // ), + // Text( + // (widget.subject.isRenamed + // ? widget.subject.renamedTo + // : widget.subject.name) ?? + // '', + // style: const TextStyle( + // fontSize: 17.0, + // fontWeight: FontWeight.w500, + // ), + // ) + // ], + // ) + // ], + // ) + const SizedBox(height: 24.0), + Text( + "pick_route".i18n, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + ), + const SizedBox(height: 12.0), + if (recommended != null) + RouteOption( + plan: recommended!, + mark: RouteMark.recommended, + selected: selectedRoute == recommended!, + onSelected: () => setState(() { + selectedRoute = recommended; + }), + ), + if (fastest != null && fastest != recommended) + RouteOption( + plan: fastest!, + mark: RouteMark.fastest, + selected: selectedRoute == fastest!, + onSelected: () => setState(() { + selectedRoute = fastest; + }), + ), + ...otherPlans.map((e) => RouteOption( + plan: e, + selected: selectedRoute == e, + onSelected: () => setState(() { + selectedRoute = e; + }), + )), + if (result != PlanResult.available) Text(result.name), + ], + ), + ), + ], + ), + ), + bottomSheet: MediaQuery.removePadding( + context: context, + removeBottom: false, + removeTop: true, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Container( + padding: const EdgeInsets.only(top: 24.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(.1), + blurRadius: 8.0, + ) + ]), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GoalInput( + value: goalValue, + currentAverage: currentAvg, + onChanged: (v) => setState(() { + selectedRoute = null; + goalValue = v; + }), + ), + const SizedBox(height: 24.0), + SizedBox( + width: double.infinity, + child: RawMaterialButton( + onPressed: () async { + if (selectedRoute == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('${"pick_route".i18n}...'))); + } + + final goalPlans = await fetchGoalPlans(); + final goalAvgs = await fetchGoalAverages(); + final goalBeforeGrades = await fetchGoalBees(); + + goalPlans[widget.subject.id] = + selectedRoute!.dbString; + goalAvgs[widget.subject.id] = + goalValue.toStringAsFixed(1); + goalBeforeGrades[widget.subject.id] = + avg.toStringAsFixed(1); + + await dbProvider.userStore.storeSubjectGoalPlans( + goalPlans, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalAverages( + goalAvgs, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalBefores( + goalBeforeGrades, + userId: user.id!); + + Navigator.of(context).pop(); + }, + fillColor: Theme.of(context).colorScheme.primary, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + "track_it".i18n, + style: const TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart new file mode 100644 index 0000000..d797fe9 --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart @@ -0,0 +1,343 @@ +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/helpers/average_helper.dart'; +import 'package:filcnaplo/helpers/subject.dart'; +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_kreta_api/models/subject.dart'; +import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; +import 'package:filcnaplo_mobile_ui/common/progress_bar.dart'; +import 'package:filcnaplo_mobile_ui/common/round_border_icon.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_state_screen.i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +import 'graph.dart'; + +class GoalStateScreen extends StatefulWidget { + final Subject subject; + + const GoalStateScreen({Key? key, required this.subject}) : super(key: key); + + @override + State createState() => _GoalStateScreenState(); +} + +class _GoalStateScreenState extends State { + late UserProvider user; + late DatabaseProvider db; + late GradeProvider gradeProvider; + late SettingsProvider settingsProvider; + + double currAvg = 0.0; + double goalAvg = 0.0; + double beforeAvg = 0.0; + double avgDifference = 0; + + late Widget gradeGraph; + + DateTime goalPinDate = DateTime.now(); + + void fetchGoalAverages() async { + var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); + var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.id!); + + goalPinDate = DateTime.parse((await db.userQuery + .subjectGoalPinDates(userId: user.id!))[widget.subject.id]!); + + String? goalAvgStr = goalAvgRes[widget.subject.id]; + String? beforeAvgStr = beforeAvgRes[widget.subject.id]; + goalAvg = double.parse(goalAvgStr ?? '0.0'); + beforeAvg = double.parse(beforeAvgStr ?? '0.0'); + + avgDifference = ((goalAvg - beforeAvg) / beforeAvg.abs()) * 100; + + setState(() {}); + } + + List getSubjectGrades(Subject subject) => gradeProvider.grades + .where((e) => (e.subject == subject && e.date.isAfter(goalPinDate))) + .toList(); + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + db = Provider.of(context, listen: false); + + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchGoalAverages(); + }); + } + + @override + Widget build(BuildContext context) { + gradeProvider = Provider.of(context); + settingsProvider = Provider.of(context); + + List subjectGrades = getSubjectGrades(widget.subject).toList(); + currAvg = AverageHelper.averageEvals(subjectGrades); + + Color averageColor = currAvg >= 1 && currAvg <= 5 + ? ColorTween( + begin: settingsProvider.gradeColors[currAvg.floor() - 1], + end: settingsProvider.gradeColors[currAvg.ceil() - 1]) + .transform(currAvg - currAvg.floor())! + : Theme.of(context).colorScheme.secondary; + + gradeGraph = Padding( + padding: const EdgeInsets.only( + top: 12.0, + bottom: 8.0, + ), + child: Panel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(top: 16.0, right: 12.0), + child: + GoalGraph(subjectGrades, dayThreshold: 5, classAvg: goalAvg), + ), + const SizedBox(height: 5.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'look_at_graph'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 23.0, + ), + ), + Text( + 'thats_progress'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 20.0, + ), + ), + const SizedBox(height: 15.0), + ProgressBar( + value: currAvg / goalAvg, + backgroundColor: averageColor, + height: 16.0, + ), + const SizedBox(height: 8.0), + ], + ), + ), + ], + ), + ), + ); + + return Scaffold( + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/subject_covers/math_light.png'), + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, + ), + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.2), + Theme.of(context).scaffoldBackgroundColor, + ], + stops: const [ + 0.1, + 0.22, + ], + ), + ), + child: Padding( + padding: const EdgeInsets.only(top: 10.0, left: 2.0, right: 2.0), + child: ListView( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + BackButton(), + ], + ), + const SizedBox(height: 22.0), + Column( + children: [ + RoundBorderIcon( + icon: Icon( + SubjectIcon.resolveVariant( + context: context, + subject: widget.subject, + ), + size: 26.0, + weight: 2.5, + ), + padding: 8.0, + width: 2.5, + ), + const SizedBox( + height: 10.0, + ), + Text( + (widget.subject.isRenamed + ? widget.subject.renamedTo + : widget.subject.name) ?? + 'goal_planner_title'.i18n, + style: const TextStyle( + fontSize: 30.0, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'almost_there'.i18n, + style: const TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.w400, + height: 1.0, + ), + ), + ], + ), + const SizedBox(height: 28.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'started_with'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + ), + ], + ), + Row( + children: [ + Text( + 'current'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 10.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Panel( + padding: const EdgeInsets.all(18.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'your_goal'.i18n, + style: const TextStyle( + fontSize: 23.0, + fontWeight: FontWeight.w700, + ), + ), + RawMaterialButton( + onPressed: () async {}, + fillColor: Colors.black, + shape: const StadiumBorder(), + padding: + const EdgeInsets.symmetric(horizontal: 18.0), + child: Text( + "change_it".i18n, + style: const TextStyle( + height: 1.0, + color: Colors.white, + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + goalAvg.toString(), + style: const TextStyle( + height: 1.1, + fontSize: 42.0, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(width: 10.0), + Center( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(45.0), + color: Colors.greenAccent.shade700 + .withOpacity(.15), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + FeatherIcons.chevronUp, + color: Colors.greenAccent.shade700, + size: 18.0, + ), + const SizedBox(width: 5.0), + Text( + avgDifference.toStringAsFixed(2) + '%', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.greenAccent.shade700, + fontSize: 22.0, + height: 0.8, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 5.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: gradeGraph, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart new file mode 100644 index 0000000..90fabd5 --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart @@ -0,0 +1,39 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "goal_planner_title": "Goal Planning", + "almost_there": "Almost there! Keep going!", + "select_subject": "Subject", + "pick_route": "Pick a Route", + "track_it": "Track it!", + "recommended": "Recommended", + "fastest": "Fastest", + }, + "hu_hu": { + "goal_planner_title": "Cél követés", + "set_a_goal": "Kitűzött cél", + "select_subject": "Tantárgy", + "pick_route": "Válassz egy utat", + "track_it": "Követés!", + "recommended": "Ajánlott", + "fastest": "Leggyorsabb", + }, + "de_de": { + "goal_planner_title": "Zielplanung", + "set_a_goal": "Dein Ziel", + "select_subject": "Thema", + "pick_route": "Wähle einen Weg", + "track_it": "Verfolge es!", + "recommended": "Empfohlen", + "fastest": "Am schnellsten", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.dart new file mode 100644 index 0000000..724b84a --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.dart @@ -0,0 +1,249 @@ +import 'dart:math'; + +import 'package:filcnaplo/helpers/average_helper.dart'; +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_premium/ui/mobile/goal_planner/graph.i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:provider/provider.dart'; + +class GoalGraph extends StatefulWidget { + const GoalGraph(this.data, {Key? key, this.dayThreshold = 7, this.classAvg}) + : super(key: key); + + final List data; + final int dayThreshold; + final double? classAvg; + + @override + _GoalGraphState createState() => _GoalGraphState(); +} + +class _GoalGraphState extends State { + late SettingsProvider settings; + + List getSpots(List data) { + List subjectData = []; + List> sortedData = [[]]; + + // Sort by date descending + data.sort((a, b) => -a.writeDate.compareTo(b.writeDate)); + + // Sort data to points by treshold + for (var element in data) { + if (sortedData.last.isNotEmpty && + sortedData.last.last.writeDate.difference(element.writeDate).inDays > + widget.dayThreshold) { + sortedData.add([]); + } + for (var dataList in sortedData) { + dataList.add(element); + } + } + + // Create FlSpots from points + for (var dataList in sortedData) { + double average = AverageHelper.averageEvals(dataList); + + if (dataList.isNotEmpty) { + subjectData.add(FlSpot( + dataList[0].writeDate.month + + (dataList[0].writeDate.day / 31) + + ((dataList[0].writeDate.year - data.last.writeDate.year) * 12), + double.parse(average.toStringAsFixed(2)), + )); + } + } + + return subjectData; + } + + @override + Widget build(BuildContext context) { + settings = Provider.of(context); + + List subjectSpots = []; + List ghostSpots = []; + List extraLinesV = []; + List extraLinesH = []; + + // Filter data + List data = widget.data + .where((e) => e.value.weight != 0) + .where((e) => e.type == GradeType.midYear) + .where((e) => e.gradeType?.name == "Osztalyzat") + .toList(); + + // Filter ghost data + List ghostData = widget.data + .where((e) => e.value.weight != 0) + .where((e) => e.type == GradeType.ghost) + .toList(); + + // Calculate average + double average = AverageHelper.averageEvals(data); + + // Calculate graph color + Color averageColor = average >= 1 && average <= 5 + ? ColorTween( + begin: settings.gradeColors[average.floor() - 1], + end: settings.gradeColors[average.ceil() - 1]) + .transform(average - average.floor())! + : Theme.of(context).colorScheme.secondary; + + subjectSpots = getSpots(data); + + // naplo/#73 + if (subjectSpots.isNotEmpty) { + ghostSpots = getSpots(data + ghostData); + + // hax + ghostSpots = ghostSpots + .where((e) => e.x >= subjectSpots.map((f) => f.x).reduce(max)) + .toList(); + ghostSpots = ghostSpots.map((e) => FlSpot(e.x + 0.1, e.y)).toList(); + ghostSpots.add(subjectSpots.firstWhere( + (e) => e.x >= subjectSpots.map((f) => f.x).reduce(max), + orElse: () => const FlSpot(-1, -1))); + ghostSpots.removeWhere( + (element) => element.x == -1 && element.y == -1); // naplo/#74 + } + + // Horizontal line displaying the class average + if (widget.classAvg != null && + widget.classAvg! > 0.0 && + settings.graphClassAvg) { + extraLinesH.add(HorizontalLine( + y: widget.classAvg!, + color: AppColors.of(context).text.withOpacity(.75), + )); + } + + // LineChart is really cute because it tries to render it's contents outside of it's rect. + return widget.data.length <= 2 + ? SizedBox( + height: 150, + child: Center( + child: Text( + "not_enough_grades".i18n, + textAlign: TextAlign.center, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ) + : ClipRect( + child: SizedBox( + child: subjectSpots.length > 1 + ? Padding( + padding: const EdgeInsets.only(top: 8.0, right: 8.0), + child: LineChart( + LineChartData( + extraLinesData: ExtraLinesData( + verticalLines: extraLinesV, + horizontalLines: extraLinesH), + lineBarsData: [ + LineChartBarData( + preventCurveOverShooting: true, + spots: subjectSpots, + isCurved: true, + colors: [averageColor], + barWidth: 8, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + colors: [ + averageColor.withOpacity(0.7), + averageColor.withOpacity(0.3), + averageColor.withOpacity(0.2), + averageColor.withOpacity(0.1), + ], + gradientColorStops: [0.1, 0.6, 0.8, 1], + gradientFrom: const Offset(0, 0), + gradientTo: const Offset(0, 1), + ), + ), + if (ghostData.isNotEmpty && ghostSpots.isNotEmpty) + LineChartBarData( + preventCurveOverShooting: true, + spots: ghostSpots, + isCurved: true, + colors: [AppColors.of(context).text], + barWidth: 8, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + colors: [ + AppColors.of(context).text.withOpacity(0.7), + AppColors.of(context).text.withOpacity(0.3), + AppColors.of(context).text.withOpacity(0.2), + AppColors.of(context).text.withOpacity(0.1), + ], + gradientColorStops: [0.1, 0.6, 0.8, 1], + gradientFrom: const Offset(0, 0), + gradientTo: const Offset(0, 1), + ), + ), + ], + minY: 1, + maxY: 5, + gridData: FlGridData( + show: true, + horizontalInterval: 1, + // checkToShowVerticalLine: (_) => false, + // getDrawingHorizontalLine: (_) => FlLine( + // color: AppColors.of(context).text.withOpacity(.15), + // strokeWidth: 2, + // ), + // getDrawingVerticalLine: (_) => FlLine( + // color: AppColors.of(context).text.withOpacity(.25), + // strokeWidth: 2, + // ), + ), + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + tooltipBgColor: Colors.grey.shade800, + fitInsideVertically: true, + fitInsideHorizontally: true, + ), + handleBuiltInTouches: true, + touchSpotThreshold: 20.0, + getTouchedSpotIndicator: (_, spots) { + return List.generate( + spots.length, + (index) => TouchedSpotIndicatorData( + FlLine( + color: Colors.grey.shade900, + strokeWidth: 3.5, + ), + FlDotData( + getDotPainter: (a, b, c, d) => + FlDotCirclePainter( + strokeWidth: 0, + color: Colors.grey.shade900, + radius: 10.0, + ), + ), + ), + ); + }, + ), + borderData: FlBorderData( + show: false, + border: Border.all( + color: Theme.of(context).scaffoldBackgroundColor, + width: 4, + ), + ), + ), + ), + ) + : null, + height: 158, + ), + ); + } +} diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.i18n.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.i18n.dart new file mode 100644 index 0000000..50e2ea8 --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.i18n.dart @@ -0,0 +1,21 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "not_enough_grades": "Not enough data to show a graph here.", + }, + "hu_hu": { + "not_enough_grades": "Nem szereztél még elég jegyet grafikon mutatáshoz.", + }, + "de_de": { + "not_enough_grades": "Noch nicht genug Noten, um die Grafik zu zeigen.", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart index 8c7f9ca..3d02d26 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart @@ -1,3 +1,4 @@ +import 'package:filcnaplo/theme/colors/colors.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/grade_display.dart'; @@ -57,7 +58,7 @@ class RouteOption extends StatelessWidget { style: TextStyle( fontSize: 22.0, fontWeight: FontWeight.w500, - color: Colors.black.withOpacity(.7), + color: AppColors.of(context).text.withOpacity(.7), ), ), const SizedBox(width: 4.0), @@ -74,7 +75,8 @@ class RouteOption extends StatelessWidget { height: 36.0, width: 32.0, child: Center( - child: Icon(Icons.add, color: Colors.black.withOpacity(.5))), + child: Icon(Icons.add, + color: AppColors.of(context).text.withOpacity(.5))), )); } } diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart deleted file mode 100644 index 7327b9e..0000000 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart +++ /dev/null @@ -1,310 +0,0 @@ -import 'package:filcnaplo/helpers/subject.dart'; -import 'package:filcnaplo/models/settings.dart'; -import 'package:filcnaplo_kreta_api/models/grade.dart'; -import 'package:filcnaplo_kreta_api/models/subject.dart'; -import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; -import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; -import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart'; -import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart'; -import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; -import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:filcnaplo_mobile_ui/common/beta_chip.dart'; - -enum PlanResult { - available, // There are possible solutions - unreachable, // The solutions are too hard don't even try - unsolvable, // There are no solutions - reached, // Goal already reached -} - -class GoalPlannerTest extends StatefulWidget { - final Subject subject; - - const GoalPlannerTest({Key? key, required this.subject}) : super(key: key); - - @override - State createState() => _GoalPlannerTestState(); -} - -class _GoalPlannerTestState extends State { - late GradeProvider gradeProvider; - late GradeCalculatorProvider calculatorProvider; - late SettingsProvider settingsProvider; - - bool gradeCalcMode = false; - - List getSubjectGrades(Subject subject) => !gradeCalcMode - ? gradeProvider.grades.where((e) => e.subject == subject).toList() - : calculatorProvider.grades.where((e) => e.subject == subject).toList(); - - double goalValue = 4.0; - List grades = []; - - Plan? recommended; - Plan? fastest; - Plan? selectedRoute; - List otherPlans = []; - - PlanResult getResult() { - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - recommended = null; - fastest = null; - otherPlans = []; - - if (currentAvg >= goalValue) return PlanResult.reached; - - final planner = GoalPlanner(goalValue, grades); - final plans = planner.solve(); - - plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) - .abs() - .compareTo(b.avg - (2 * goalValue + 5) / 3)); - - try { - final singleSolution = plans.every((e) => e.sigma == 0); - recommended = - plans.where((e) => singleSolution ? true : e.sigma > 0).first; - plans.removeWhere((e) => e == recommended); - } catch (_) {} - - plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); - - try { - fastest = plans.removeAt(0); - } catch (_) {} - - if ((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0) >= 3) { - recommended = fastest; - } - - if (recommended == null) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unsolvable; - } - - if (recommended!.plan.length > 10) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unreachable; - } - - otherPlans = List.from(plans); - - return PlanResult.available; - } - - void getGrades() { - grades = getSubjectGrades(widget.subject).toList(); - } - - @override - Widget build(BuildContext context) { - gradeProvider = Provider.of(context); - calculatorProvider = Provider.of(context); - settingsProvider = Provider.of(context); - - getGrades(); - - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - final result = getResult(); - - return Scaffold( - body: SafeArea( - child: ListView( - padding: const EdgeInsets.only( - left: 22.0, right: 22.0, top: 5.0, bottom: 220.0), - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BackButton(), - Padding( - padding: const EdgeInsets.only(right: 15.0), - child: Row( - children: [ - Text( - 'goal_planner_title'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w500, fontSize: 18.0), - ), - const SizedBox( - width: 5, - ), - const BetaChip(), - ], - ), - ), - ], - ), - const SizedBox(height: 12.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "set_a_goal".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), - const SizedBox(height: 4.0), - Text( - goalValue.toString(), - style: TextStyle( - fontWeight: FontWeight.w900, - fontSize: 42.0, - color: gradeColor(goalValue.round(), settingsProvider), - ), - ), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "select_subject".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), - const SizedBox(height: 4.0), - Column( - children: [ - Icon( - SubjectIcon.resolveVariant( - context: context, - subject: widget.subject, - ), - size: 48.0, - ), - Text( - (widget.subject.isRenamed - ? widget.subject.renamedTo - : widget.subject.name) ?? - '', - style: const TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.w500, - ), - ) - ], - ) - ], - ) - ], - ), - const SizedBox(height: 24.0), - Text( - "pick_route".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), - const SizedBox(height: 12.0), - if (recommended != null) - RouteOption( - plan: recommended!, - mark: RouteMark.recommended, - selected: selectedRoute == recommended!, - onSelected: () => setState(() { - selectedRoute = recommended; - }), - ), - if (fastest != null && fastest != recommended) - RouteOption( - plan: fastest!, - mark: RouteMark.fastest, - selected: selectedRoute == fastest!, - onSelected: () => setState(() { - selectedRoute = fastest; - }), - ), - ...otherPlans.map((e) => RouteOption( - plan: e, - selected: selectedRoute == e, - onSelected: () => setState(() { - selectedRoute = e; - }), - )), - if (result != PlanResult.available) Text(result.name), - ], - ), - ), - bottomSheet: MediaQuery.removePadding( - context: context, - removeBottom: false, - removeTop: true, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Container( - padding: const EdgeInsets.only(top: 24.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(24.0)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(.1), - blurRadius: 8.0, - ) - ]), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GoalInput( - value: goalValue, - currentAverage: currentAvg, - onChanged: (v) => setState(() { - selectedRoute = null; - goalValue = v; - }), - ), - const SizedBox(height: 24.0), - SizedBox( - width: double.infinity, - child: RawMaterialButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Hamarosan..."))); - }, - fillColor: Theme.of(context).colorScheme.primary, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "track_it".i18n, - style: const TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.w600, - ), - ), - ), - ) - ], - ), - ), - ), - ), - ), - ), - ); - } -}