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 6b1dec6..9e68f47 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 8ba3974..3bae643 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 ced8ee0..df07316 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 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 ``` 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 0000000..498f473 --- /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 3b621fd..62e8b45 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 0000000..b24b5b1 --- /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 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 index 7327b9e..401f6a5 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 2cede66..8c01136 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 fb892f6..4706615 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 86330b4..1319286 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 b24b5b1..f8c9220 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 5b87c8e..29217c8 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 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/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 401f6a5..3db2e2d 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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 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 4706615..b3a6de8 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 1319286..4083bec 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 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_mobile_ui/lib/common/round_border_icon.dart b/filcnaplo_mobile_ui/lib/common/round_border_icon.dart index f8c9220..ac1fdf4 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 29217c8..0c0c152 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 3db2e2d..4cf8acf 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 0000000..0397160 --- /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 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); +} 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 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 1519f0a..e87209c 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 b3a6de8..fb3094c 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 4083bec..185462d 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 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_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_state_screen.dart index 0397160..d797fe9 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 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); +}