From 63e5ccb7c30f0a5f138ee36790ffadbf9f77c82e Mon Sep 17 00:00:00 2001 From: Kima Date: Sat, 26 Aug 2023 13:27:58 +0200 Subject: [PATCH 1/9] added rounded expansion tile (fixed absence bug) --- filcnaplo/ios/Runner.xcodeproj/project.pbxproj | 2 +- .../lib/common/widgets/absence_group/absence_group_tile.dart | 3 ++- filcnaplo_mobile_ui/pubspec.yaml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/filcnaplo/ios/Runner.xcodeproj/project.pbxproj b/filcnaplo/ios/Runner.xcodeproj/project.pbxproj index 6b1dec6b..9e68f47d 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_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart index 8ba39740..3bae6430 100755 --- a/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart +++ b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart @@ -6,6 +6,7 @@ import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart'; import 'package:filcnaplo/utils/format.dart'; import 'package:flutter/material.dart'; import 'absence_group_tile.i18n.dart'; +import 'package:rounded_expansion_tile/rounded_expansion_tile.dart'; class AbsenceGroupTile extends StatelessWidget { const AbsenceGroupTile(this.absences, @@ -32,7 +33,7 @@ class AbsenceGroupTile extends StatelessWidget { child: Padding( padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), child: AbsenceGroupContainer( - child: ExpansionTile( + child: RoundedExpansionTile( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), diff --git a/filcnaplo_mobile_ui/pubspec.yaml b/filcnaplo_mobile_ui/pubspec.yaml index ced8ee0c..df073161 100755 --- a/filcnaplo_mobile_ui/pubspec.yaml +++ b/filcnaplo_mobile_ui/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: dotted_border: ^2.0.0+3 screenshot: ^2.1.0 image_gallery_saver: ^2.0.2 + rounded_expansion_tile: ^0.0.13 dev_dependencies: flutter_lints: ^1.0.0 From 534a223cbf9145d0646ffd50363eaedbbfbf95dc Mon Sep 17 00:00:00 2001 From: ReinerRego <59338514+ReinerRego@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:10:25 +0200 Subject: [PATCH 2/9] Update README.md --recursive kiszedve mert nem kell --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 117db862..c7e5ae90 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 ``` From 4b70b6e03509b9144472baf6e848a9274be056e6 Mon Sep 17 00:00:00 2001 From: Kima Date: Sun, 27 Aug 2023 21:42:47 +0200 Subject: [PATCH 3/9] added back pubspec xd --- filcnaplo_mobile_ui/pubspec.yaml | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 filcnaplo_mobile_ui/pubspec.yaml diff --git a/filcnaplo_mobile_ui/pubspec.yaml b/filcnaplo_mobile_ui/pubspec.yaml new file mode 100644 index 00000000..498f4730 --- /dev/null +++ b/filcnaplo_mobile_ui/pubspec.yaml @@ -0,0 +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 \ No newline at end of file From 1314b2f06887639c0ab930a1217f509bf86cf014 Mon Sep 17 00:00:00 2001 From: Kima Date: Sun, 27 Aug 2023 23:48:11 +0200 Subject: [PATCH 4/9] fix import --- .../lib/common/widgets/absence_group/absence_group_tile.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart index 3b621fd5..62e8b458 100755 --- a/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart +++ b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:rounded_expansion_tile/rounded_expansion_tile.dart'; import 'absence_group_tile.i18n.dart'; -import 'package:rounded_expansion_tile/rounded_expansion_tile.dart'; class AbsenceGroupTile extends StatelessWidget { const AbsenceGroupTile(this.absences, From 09e416ab74d07258fa2643cc7ebe8ba02ccb91fd Mon Sep 17 00:00:00 2001 From: Kima Date: Sun, 27 Aug 2023 23:58:32 +0200 Subject: [PATCH 5/9] added rounded border icon (new style thing) --- .../lib/common/round_border_icon.dart | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 filcnaplo_mobile_ui/lib/common/round_border_icon.dart 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 00000000..b24b5b1e --- /dev/null +++ b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class RoundBorderIcon extends StatelessWidget { + final Color color; + final double width; + final Widget icon; + + const RoundBorderIcon( + {Key? key, + this.color = Colors.black, + this.width = 16.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.zero, + child: icon, + ), + ); + } +} From 3e470981a8d9580e147e6f4db00992877e80cd21 Mon Sep 17 00:00:00 2001 From: Kima Date: Mon, 28 Aug 2023 00:36:24 +0200 Subject: [PATCH 6/9] made goal planner even better --- .../ui/mobile/goal_planner/route_option.dart | 6 +- .../lib/ui/mobile/goal_planner/test.dart | 152 ++++++++++++------ 2 files changed, 105 insertions(+), 53 deletions(-) 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 8c7f9caf..3d02d26b 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 index 7327b9e9..401f6a51 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart @@ -1,8 +1,12 @@ +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'; @@ -10,7 +14,6 @@ import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18 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 @@ -117,31 +120,78 @@ class _GoalPlannerTestState extends State { 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( 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(), + // ], + // ), + // ), + // ], + // ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, 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), + Row( + children: [ + const BackButton(), + RoundBorderIcon( + icon: Icon( + SubjectIcon.resolveVariant( + context: context, + subject: widget.subject, + ), + size: 10, ), - const SizedBox( - width: 5, + ), + Text( + (widget.subject.isRenamed + ? widget.subject.renamedTo + : widget.subject.name) ?? + 'goal_planner_title'.i18n, + style: const TextStyle( + fontSize: 17.0, + fontWeight: FontWeight.w500, ), - const BetaChip(), - ], - ), + ), + ], + ), + Row( + children: [ + if (groupAverage != 0) + AverageDisplay(average: groupAverage, border: true), + const SizedBox(width: 6.0), + AverageDisplay(average: avg) + ], ), ], ), @@ -151,7 +201,7 @@ class _GoalPlannerTestState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, children: [ Text( "set_a_goal".i18n, @@ -171,40 +221,40 @@ class _GoalPlannerTestState extends State { ), ], ), - 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, - ), - ) - ], - ) - ], - ) + // 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), @@ -254,7 +304,7 @@ class _GoalPlannerTestState extends State { child: Container( padding: const EdgeInsets.only(top: 24.0), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.vertical(top: Radius.circular(24.0)), boxShadow: [ From 0ac0586fba5c357551956ee925ba09b76ff35078 Mon Sep 17 00:00:00 2001 From: Kima Date: Mon, 28 Aug 2023 02:33:39 +0200 Subject: [PATCH 7/9] setted goal plans can be saved now --- filcnaplo/lib/database/init.dart | 4 + filcnaplo/lib/database/query.dart | 11 + filcnaplo/lib/database/store.dart | 7 + .../lib/common/round_border_icon.dart | 4 +- .../lib/pages/grades/grade_subject_view.dart | 57 +- .../ui/mobile/goal_planner/goal_planner.dart | 8 + .../{test.dart => goal_planner_screen.dart} | 740 +++++++++--------- 7 files changed, 467 insertions(+), 364 deletions(-) rename filcnaplo_premium/lib/ui/mobile/goal_planner/{test.dart => goal_planner_screen.dart} (74%) diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 2cede663..8c01136f 100644 --- a/filcnaplo/lib/database/init.dart +++ b/filcnaplo/lib/database/init.dart @@ -46,6 +46,8 @@ const userDataDB = DatabaseStruct("user_data", { "renamed_teachers": String, // "subject_lesson_count": String, // non kreta data "last_seen_grade": int, + // goal plans // non kreta data + "goal_plans": String, }); Future createTable(Database db, DatabaseStruct struct) => @@ -97,6 +99,8 @@ Future initDB(DatabaseProvider database) async { "renamed_teachers": "{}", // "subject_lesson_count": "{}", // non kreta data "last_seen_grade": 0, + // goal plans // non kreta data + "goal_plans": "{}", }); } catch (error) { print("ERROR: migrateDB: $error"); diff --git a/filcnaplo/lib/database/query.dart b/filcnaplo/lib/database/query.dart index fb892f65..47066150 100644 --- a/filcnaplo/lib/database/query.dart +++ b/filcnaplo/lib/database/query.dart @@ -213,4 +213,15 @@ class UserDatabaseQuery { return (jsonDecode(renamedTeachersJson) as Map) .map((key, value) => MapEntry(key.toString(), value.toString())); } + + 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())); + } } diff --git a/filcnaplo/lib/database/store.dart b/filcnaplo/lib/database/store.dart index 86330b4b..13192861 100644 --- a/filcnaplo/lib/database/store.dart +++ b/filcnaplo/lib/database/store.dart @@ -140,4 +140,11 @@ class UserDatabaseStore { await db.update("user_data", {"renamed_teachers": renamedTeachersJson}, where: "id = ?", whereArgs: [userId]); } + + Future storeSubjectGoalPlans(Map plans, + {required String userId}) async { + String goalPlansJson = jsonEncode(plans); + await db.update("user_data", {"goal_plans": goalPlansJson}, + where: "id = ?", whereArgs: [userId]); + } } diff --git a/filcnaplo_mobile_ui/lib/common/round_border_icon.dart b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart index b24b5b1e..f8c92203 100644 --- a/filcnaplo_mobile_ui/lib/common/round_border_icon.dart +++ b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart @@ -8,7 +8,7 @@ class RoundBorderIcon extends StatelessWidget { const RoundBorderIcon( {Key? key, this.color = Colors.black, - this.width = 16.0, + this.width = 1.5, required this.icon}) : super(key: key); @@ -20,7 +20,7 @@ class RoundBorderIcon extends StatelessWidget { borderRadius: BorderRadius.circular(50.0), ), child: Padding( - padding: EdgeInsets.zero, + padding: const EdgeInsets.all(5.0), 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 5b87c8ef..29217c80 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,7 +23,7 @@ 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/premium/upsell.dart'; @@ -62,12 +64,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 +157,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 +216,8 @@ class _GradeSubjectViewState extends State { buildTiles(ghostGrades); } + fetchGoalPlans(); + return Scaffold( key: _scaffoldKey, floatingActionButtonLocation: ExpandableFab.location, @@ -213,6 +235,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 +243,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 +259,7 @@ class _GradeSubjectViewState extends State { Navigator.of(context).push(CupertinoPageRoute( builder: (context) => - GoalPlannerTest(subject: widget.subject))); + GoalPlannerScreen(subject: widget.subject))); }, ), ], @@ -261,6 +285,35 @@ 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) => + GoalPlannerScreen(subject: widget.subject))); + }, + child: Container( + width: 54.0, + padding: const EdgeInsets.symmetric( + horizontal: 5.0, 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_premium/lib/ui/mobile/goal_planner/goal_planner.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart index 52b4a93f..22878a94 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/test.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart similarity index 74% rename from filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart rename to filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart index 401f6a51..3db2e2d6 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart @@ -1,360 +1,380 @@ -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 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(); - - 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( - 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(), - // ], - // ), - // ), - // ], - // ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - const BackButton(), - RoundBorderIcon( - icon: Icon( - SubjectIcon.resolveVariant( - context: context, - subject: widget.subject, - ), - size: 10, - ), - ), - Text( - (widget.subject.isRenamed - ? widget.subject.renamedTo - : widget.subject.name) ?? - 'goal_planner_title'.i18n, - style: const TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - Row( - children: [ - if (groupAverage != 0) - AverageDisplay(average: groupAverage, border: true), - const SizedBox(width: 6.0), - AverageDisplay(average: avg) - ], - ), - ], - ), - const SizedBox(height: 12.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.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: 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: 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: () { - 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, - ), - ), - ), - ) - ], - ), - ), - ), - ), - ), - ), - ); - } -} +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!); + } + + 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( + 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(), + // ], + // ), + // ), + // ], + // ), + 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), + 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(); + goalPlans[widget.subject.id] = + selectedRoute!.dbString; + + await dbProvider.userStore.storeSubjectGoalPlans( + goalPlans, + 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, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ), + ), + ); + } +} From d524d452bddebd05a21a9e103d674505310f28ac Mon Sep 17 00:00:00 2001 From: Kima Date: Mon, 28 Aug 2023 18:25:20 +0200 Subject: [PATCH 8/9] some progress in goal planner (started goal state) --- .../images/subject_covers/math_light.png | Bin 0 -> 19672 bytes filcnaplo/lib/database/init.dart | 8 +- filcnaplo/lib/database/query.dart | 27 ++- filcnaplo/lib/database/store.dart | 16 ++ filcnaplo/pubspec.yaml | 1 + .../lib/common/round_border_icon.dart | 4 +- .../lib/pages/grades/grade_subject_view.dart | 6 +- .../goal_planner/goal_planner_screen.dart | 202 ++++++++------- .../goal_planner/goal_state_screen.dart | 229 ++++++++++++++++++ .../goal_planner/goal_state_screen.i18n.dart | 39 +++ 10 files changed, 440 insertions(+), 92 deletions(-) create mode 100644 filcnaplo/assets/images/subject_covers/math_light.png create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart 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 0000000000000000000000000000000000000000..eb3e2f518951b484985709a3292f0c0d3fc50c07 GIT binary patch literal 19672 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 literal 0 HcmV?d00001 diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 8c01136f..1519f0a6 100644 --- a/filcnaplo/lib/database/init.dart +++ b/filcnaplo/lib/database/init.dart @@ -46,8 +46,10 @@ const userDataDB = DatabaseStruct("user_data", { "renamed_teachers": String, // "subject_lesson_count": String, // non kreta data "last_seen_grade": int, - // goal plans // non kreta data + // goal planning // non kreta data "goal_plans": String, + "goal_averages": String, + "goal_befores": String, }); Future createTable(Database db, DatabaseStruct struct) => @@ -99,8 +101,10 @@ Future initDB(DatabaseProvider database) async { "renamed_teachers": "{}", // "subject_lesson_count": "{}", // non kreta data "last_seen_grade": 0, - // goal plans // non kreta data + // goal planning // non kreta data "goal_plans": "{}", + "goal_averages": "{}", + "goal_befores": "{}", }); } catch (error) { print("ERROR: migrateDB: $error"); diff --git a/filcnaplo/lib/database/query.dart b/filcnaplo/lib/database/query.dart index 47066150..b3a6de85 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]); @@ -214,14 +215,36 @@ class UserDatabaseQuery { .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?; + 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())); + } } diff --git a/filcnaplo/lib/database/store.dart b/filcnaplo/lib/database/store.dart index 13192861..4083bec0 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); @@ -141,10 +142,25 @@ class UserDatabaseStore { 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]); + } } diff --git a/filcnaplo/pubspec.yaml b/filcnaplo/pubspec.yaml index be6afaab..751aa3e4 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_mobile_ui/lib/common/round_border_icon.dart b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart index f8c92203..ac1fdf47 100644 --- a/filcnaplo_mobile_ui/lib/common/round_border_icon.dart +++ b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart @@ -3,12 +3,14 @@ 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); @@ -20,7 +22,7 @@ class RoundBorderIcon extends StatelessWidget { borderRadius: BorderRadius.circular(50.0), ), child: Padding( - padding: const EdgeInsets.all(5.0), + 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 29217c80..0c0c1522 100755 --- a/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart +++ b/filcnaplo_mobile_ui/lib/pages/grades/grade_subject_view.dart @@ -26,6 +26,7 @@ import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.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'; @@ -292,12 +293,11 @@ class _GradeSubjectViewState extends State { onTap: () { Navigator.of(context).push(CupertinoPageRoute( builder: (context) => - GoalPlannerScreen(subject: widget.subject))); + GoalStateScreen(subject: widget.subject))); }, child: Container( width: 54.0, - padding: const EdgeInsets.symmetric( - horizontal: 5.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(45.0), color: Theme.of(context) 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 index 3db2e2d6..4cf8acfe 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner_screen.dart @@ -65,6 +65,15 @@ class _GoalPlannerScreenState extends State { 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); @@ -148,7 +157,11 @@ class _GoalPlannerScreenState extends State { body: SafeArea( child: ListView( padding: const EdgeInsets.only( - left: 22.0, right: 22.0, top: 5.0, bottom: 220.0), + top: 5.0, + bottom: 220.0, + right: 15.0, + left: 2.0, + ), children: [ // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -215,91 +228,99 @@ class _GoalPlannerScreenState extends State { ], ), const SizedBox(height: 12.0), - Text( - "set_a_goal".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.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), + ], ), ), - 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), ], ), ), @@ -346,12 +367,25 @@ class _GoalPlannerScreenState extends State { } 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(); }, 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 00000000..03971600 --- /dev/null +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart @@ -0,0 +1,229 @@ +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/helpers/subject.dart'; +import 'package:filcnaplo_kreta_api/models/subject.dart'; +import 'package:filcnaplo_mobile_ui/common/average_display.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.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:provider/provider.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; + + double goalAvg = 0.0; + double beforeAvg = 0.0; + double avgDifference = 0; + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + db = Provider.of(context, listen: false); + + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchGoalAverages(); + }); + } + + void fetchGoalAverages() async { + var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); + var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.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; + } + + @override + Widget build(BuildContext context) { + 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, + 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( + children: [ + Text( + goalAvg.toString(), + style: const TextStyle( + height: 1.1, + fontSize: 42.0, + fontWeight: FontWeight.w800, + ), + ), + Center( + child: Container( + width: 54.0, + padding: + const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(45.0), + color: Colors.limeAccent.shade700 + .withOpacity(.15), + ), + child: Text(avgDifference.toString()), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} 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 00000000..90fabd59 --- /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); +} From 40dd967903aa5c4cbc98a171a94f3ec1e975c9f7 Mon Sep 17 00:00:00 2001 From: Kima Date: Tue, 29 Aug 2023 00:24:57 +0200 Subject: [PATCH 9/9] some progress in goal planner again --- filcnaplo/lib/app.dart | 3 +- filcnaplo/lib/database/init.dart | 2 + filcnaplo/lib/database/query.dart | 12 + filcnaplo/lib/database/store.dart | 7 + .../lib/providers/homework_provider.dart | 2 + .../lib/common/progress_bar.dart | 25 +- .../goal_planner/goal_state_screen.dart | 150 +++++++++-- .../lib/ui/mobile/goal_planner/graph.dart | 249 ++++++++++++++++++ .../ui/mobile/goal_planner/graph.i18n.dart | 21 ++ 9 files changed, 445 insertions(+), 26 deletions(-) create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/graph.dart create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/graph.i18n.dart diff --git a/filcnaplo/lib/app.dart b/filcnaplo/lib/app.dart index 581b4a0d..15a410c9 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 1519f0a6..e87209cc 100644 --- a/filcnaplo/lib/database/init.dart +++ b/filcnaplo/lib/database/init.dart @@ -50,6 +50,7 @@ const userDataDB = DatabaseStruct("user_data", { "goal_plans": String, "goal_averages": String, "goal_befores": String, + "goal_pin_dates": String, }); Future createTable(Database db, DatabaseStruct struct) => @@ -105,6 +106,7 @@ Future initDB(DatabaseProvider database) async { "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 b3a6de85..fb3094cd 100644 --- a/filcnaplo/lib/database/query.dart +++ b/filcnaplo/lib/database/query.dart @@ -247,4 +247,16 @@ class UserDatabaseQuery { 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 4083bec0..185462de 100644 --- a/filcnaplo/lib/database/store.dart +++ b/filcnaplo/lib/database/store.dart @@ -163,4 +163,11 @@ class UserDatabaseStore { 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_kreta_api/lib/providers/homework_provider.dart b/filcnaplo_kreta_api/lib/providers/homework_provider.dart index 8f5fa4d5..3a20e1fa 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 a89ce689..ffffa004 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_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart index 03971600..d797fe9a 100644 --- a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart +++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart @@ -1,14 +1,21 @@ 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_mobile_ui/common/average_display.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; @@ -21,11 +28,39 @@ class GoalStateScreen extends StatefulWidget { 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(); @@ -37,20 +72,70 @@ class _GoalStateScreenState extends State { }); } - void fetchGoalAverages() async { - var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); - var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.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; - } - @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( @@ -164,6 +249,7 @@ class _GoalStateScreenState extends State { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'your_goal'.i18n, @@ -191,6 +277,7 @@ class _GoalStateScreenState extends State { ], ), Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( goalAvg.toString(), @@ -200,17 +287,39 @@ class _GoalStateScreenState extends State { fontWeight: FontWeight.w800, ), ), + const SizedBox(width: 10.0), Center( child: Container( - width: 54.0, - padding: - const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric( + vertical: 5.0, + horizontal: 8.0, + ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(45.0), - color: Colors.limeAccent.shade700 + color: Colors.greenAccent.shade700 .withOpacity(.15), ), - child: Text(avgDifference.toString()), + 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, + ), + ), + ], + ), ), ), ], @@ -219,6 +328,11 @@ class _GoalStateScreenState extends State { ), ), ), + 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/graph.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/graph.dart new file mode 100644 index 00000000..724b84a8 --- /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 00000000..50e2ea8c --- /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); +}