From 77cb2ed85e980b312604941348ebf03ffafa329a Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 26 Mar 2024 22:59:00 +0300 Subject: [PATCH 1/4] chore: exam timetable basic ui --- lib/constants/common.dart | 6 +- .../exams_timetable_controller.dart | 69 ++----- .../exam_timetable/exams_timetable_page.dart | 173 ++++++++++++++++-- pubspec.yaml | 1 + 4 files changed, 180 insertions(+), 69 deletions(-) diff --git a/lib/constants/common.dart b/lib/constants/common.dart index a3b9518..1028aa4 100644 --- a/lib/constants/common.dart +++ b/lib/constants/common.dart @@ -82,12 +82,14 @@ void showCustomSnackbar( String message, { IconData? icon, Color? iconColor, + Color? backgroundColor = Colors.white, + Color? colorText = Colors.black, }) { Get.snackbar( title, message, - colorText: Colors.black, - backgroundColor: Colors.white, + colorText: colorText, + backgroundColor: backgroundColor, icon: Icon(icon ?? Icons.info), ); } diff --git a/lib/tools/exam_timetable/exams_timetable_controller.dart b/lib/tools/exam_timetable/exams_timetable_controller.dart index c6ffa26..7939e25 100644 --- a/lib/tools/exam_timetable/exams_timetable_controller.dart +++ b/lib/tools/exam_timetable/exams_timetable_controller.dart @@ -1,61 +1,26 @@ -import 'package:academia/constants/common.dart'; -import 'package:academia/models/courses.dart'; -import 'package:flutter/material.dart'; +import 'package:academia/exports/barrel.dart'; import 'package:get/get.dart'; class ExamsTimeTableController extends GetxController { - var hasExams = false.obs; - var isLoading = false.obs; - - Future> fetchExamTimeTable(String units, - {bool athi = true}) async { - isLoading.value = true; - - try { - var fetchedUnits = await magnet.fetchExamTimeTabale(units); - isLoading.value = false; - - return fetchedUnits; - } catch (e) { - Get.snackbar( - "Oh Snap!", - "Something went wrong while attempting to fetch your exam timetable, please check your network connection and try again", - icon: const Icon( - Icons.network_ping, - ), - maxWidth: 500, - ); - debugPrint(e.toString()); - } - - isLoading.value = false; - return []; - } - - Future addFetchedUnits(List fetchedUnits) async { - await appDB.put("exam_timetable", fetchedUnits); + Future fetchExams() async { + await Future.delayed(const Duration(seconds: 10)); + return; } + List userCourses = []; @override - void onInit() async { - debugPrint(appDB.get("exam_timetable").toString()); - - if (true) { - var courses = appDB.get("timetable"); - String payload = ""; - for (Courses c in courses) { - payload = - "$payload ${c.name!.replaceAll("-", " ")}${c.section!.split('-')[0]},"; - } - - debugPrint(payload); - var fetchedUnits = await fetchExamTimeTable(payload.trim()); - await addFetchedUnits(fetchedUnits); - hasExams.value = fetchedUnits.isNotEmpty; - } - - debugPrint("do I have exams : ${hasExams.value}"); - + Future onInit() async { super.onInit(); + // Load user courses + debugPrint(appDB.keys.toString()); + final List coursesData = await appDB.get("timetable"); + coursesData.forEach((element) { + if (element != null) { + userCourses.add(element); + } + }); + debugPrint("User Courses Fetched"); + // userCourses = + // courses.map((e) => Courses.fromJson(e as Map)).toList().cast(); } } diff --git a/lib/tools/exam_timetable/exams_timetable_page.dart b/lib/tools/exam_timetable/exams_timetable_page.dart index 9831933..8e0f1d8 100644 --- a/lib/tools/exam_timetable/exams_timetable_page.dart +++ b/lib/tools/exam_timetable/exams_timetable_page.dart @@ -1,29 +1,172 @@ import 'package:academia/exports/barrel.dart'; +import 'package:academia/tools/exam_timetable/exams_timetable_controller.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:get/get.dart'; -class ExamTimeTablePage extends StatelessWidget { +class ExamTimeTablePage extends StatefulWidget { const ExamTimeTablePage({super.key}); + @override + State createState() => _ExamTimeTablePageState(); +} + +class _ExamTimeTablePageState extends State { + final userController = Get.find(); + final settingsController = Get.find(); + final examtimetableController = Get.put(ExamsTimeTableController()); + final _searchController = TextEditingController(); + + bool _isSearching = false; + @override Widget build(BuildContext context) { return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Align( - alignment: Alignment.center, - child: Image.asset("assets/images/girl_sitted.png"), - ), - const Text("Exam timetable coming soon😉"), - TextButton( - onPressed: () { - Get.back(); - }, - child: const Text("Notify me when its ready"), + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text( + "Hi ${userController.user.value!.name!.split(' ')[0].title()}", + ), + actions: [ + IconButton( + onPressed: () {}, + icon: Icon(Ionicons.help), + ), + ], + expandedHeight: 250, + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 60), + const Spacer(), + Text( + "The important thing is not to stop questioning. Curiosity has its own reason for existing", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + ), + const Text( + "~ Albert Einsten", + textAlign: TextAlign.end, + ), + ], + ), + ), + ), ), + SliverFillRemaining( + hasScrollBody: true, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Seems you have no exams at the moment", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + ), + ], + ), + ), + ) ], ), + floatingActionButton: FloatingActionButton( + onPressed: () { + showModalBottomSheet( + isDismissible: false, + context: context, + builder: (context) => StatefulBuilder( + builder: (context, StateSetter setState) { + return Container( + padding: const EdgeInsets.all(16), + width: MediaQuery.of(context).size.width, + child: Column( + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: "BIL 111K, ENG 112, ...", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + ), + suffixIcon: IconButton( + onPressed: () async { + if (_searchController.text.trim().isEmpty) { + showCustomSnackbar( + "Error", + "Please provide units to search", + backgroundColor: Theme.of(context) + .colorScheme + .errorContainer, + colorText: + Theme.of(context).colorScheme.onSurface, + ); + return; + } + print("Here"); + if (mounted) { + setState(() { + _isSearching = true; + }); + + await examtimetableController.fetchExams(); + if (!mounted) { + return; + } + setState(() { + _isSearching = false; + }); + } + }, + icon: const Icon(Ionicons.search), + ), + ), + ), + const SizedBox(height: 12), + _isSearching + ? const Text("Loading") + : Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context) + .colorScheme + .tertiaryContainer, + ), + child: Text( + "Please input your units seperated with commas and wait for the magic to happen🪄", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + fontFamily: + GoogleFonts.figtree().fontFamily, + ), + ), + ), + ], + ), + ); + }, + ), + ); + }, + child: const Icon( + Ionicons.add, + ), + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 56b94a4..88da6bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: shorebird_code_push: ^1.1.3 story_view: ^0.16.0 ionicons: ^0.2.2 + google_fonts: ^6.2.1 From a41eb2a72af6a3e8d66b0fc8a0864671926bfcd4 Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 27 Mar 2024 21:11:00 +0300 Subject: [PATCH 2/4] feat: random quotes now loading --- .../exams_timetable_controller.dart | 39 ++++++++++-- .../exam_timetable/exams_timetable_page.dart | 61 +++++++++++++------ 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/lib/tools/exam_timetable/exams_timetable_controller.dart b/lib/tools/exam_timetable/exams_timetable_controller.dart index 7939e25..53d3caa 100644 --- a/lib/tools/exam_timetable/exams_timetable_controller.dart +++ b/lib/tools/exam_timetable/exams_timetable_controller.dart @@ -2,6 +2,39 @@ import 'package:academia/exports/barrel.dart'; import 'package:get/get.dart'; class ExamsTimeTableController extends GetxController { + var index = (-1).obs; + late List> quotes = []; + + Future fetchRandomQuote() async { + try { + quotes = await magnet.fetchRandomQuotes(); + index.value = 0; + } catch (e) { + showCustomSnackbar( + "Error", + e.toString(), + colorText: Colors.red, + backgroundColor: Colors.grey, + ); + } + } + + void nextQuote() { + if (quotes.isNotEmpty && index.value < 49) { + index.value++; + } else if (index.value == 49) { + fetchRandomQuote().then((value) => value); // Do nothing + } + } + + void previousQuote() { + if (quotes.isNotEmpty && index.value > 0) { + index.value--; + } else if (index.value == 0) { + fetchRandomQuote().then((value) => value); // Do nothing + } + } + Future fetchExams() async { await Future.delayed(const Duration(seconds: 10)); return; @@ -10,7 +43,7 @@ class ExamsTimeTableController extends GetxController { List userCourses = []; @override Future onInit() async { - super.onInit(); + await fetchRandomQuote(); // Load user courses debugPrint(appDB.keys.toString()); final List coursesData = await appDB.get("timetable"); @@ -19,8 +52,6 @@ class ExamsTimeTableController extends GetxController { userCourses.add(element); } }); - debugPrint("User Courses Fetched"); - // userCourses = - // courses.map((e) => Courses.fromJson(e as Map)).toList().cast(); + super.onInit(); } } diff --git a/lib/tools/exam_timetable/exams_timetable_page.dart b/lib/tools/exam_timetable/exams_timetable_page.dart index 8e0f1d8..72df6a7 100644 --- a/lib/tools/exam_timetable/exams_timetable_page.dart +++ b/lib/tools/exam_timetable/exams_timetable_page.dart @@ -13,9 +13,8 @@ class ExamTimeTablePage extends StatefulWidget { class _ExamTimeTablePageState extends State { final userController = Get.find(); final settingsController = Get.find(); - final examtimetableController = Get.put(ExamsTimeTableController()); + final controller = Get.put(ExamsTimeTableController()); final _searchController = TextEditingController(); - bool _isSearching = false; @override @@ -40,23 +39,45 @@ class _ExamTimeTablePageState extends State { color: Theme.of(context).colorScheme.primaryContainer, ), padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 60), - const Spacer(), - Text( - "The important thing is not to stop questioning. Curiosity has its own reason for existing", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - fontFamily: GoogleFonts.figtree().fontFamily, - ), - ), - const Text( - "~ Albert Einsten", - textAlign: TextAlign.end, - ), - ], + child: GestureDetector( + onPanUpdate: ((details) { + if (details.delta.dx > 0) { + controller.nextQuote(); + } + + if (details.delta.dx < 0) { + controller.previousQuote(); + } + }), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 60), + const Spacer(), + Obx( + () => Text( + controller.index > -1 + ? controller.quotes[controller.index.value]["q"] + : "So long as we are being remembered, we remain alive.", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + ), + ), + Obx( + () => Text( + controller.index > -1 + ? controller.quotes[controller.index.value]["a"] + : "Carlos Ruiz Zafon", + textAlign: TextAlign.end, + ), + ), + ], + ), ), ), ), @@ -120,7 +141,7 @@ class _ExamTimeTablePageState extends State { _isSearching = true; }); - await examtimetableController.fetchExams(); + // await examtimetableController.fetchExams(); if (!mounted) { return; } From 0fa57afd42b2b7917d6e8fb1ddfce143212abc10 Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 3 Apr 2024 23:34:00 +0300 Subject: [PATCH 3/4] feat: exam timetable now loads user courses --- lib/exports/barrel.dart | 2 + lib/main.dart | 1 + lib/models/exam.dart | 66 +++++++++++++ lib/models/exam.g.dart | 62 +++++++++++++ .../exams_timetable_controller.dart | 63 ++++++++++--- .../exam_timetable/exams_timetable_page.dart | 80 ++++++++++------ .../widgets/count_down_widget.dart | 92 +++++++++++++++++++ .../exam_timetable/widgets/exam_card.dart | 72 +++++++++++++++ pubspec.yaml | 1 + 9 files changed, 401 insertions(+), 38 deletions(-) create mode 100644 lib/models/exam.dart create mode 100644 lib/models/exam.g.dart create mode 100644 lib/tools/exam_timetable/widgets/count_down_widget.dart create mode 100644 lib/tools/exam_timetable/widgets/exam_card.dart diff --git a/lib/exports/barrel.dart b/lib/exports/barrel.dart index 1d9e964..a08722f 100644 --- a/lib/exports/barrel.dart +++ b/lib/exports/barrel.dart @@ -1,4 +1,5 @@ export 'package:flutter/material.dart'; +export 'package:academia/models/exam.dart'; export 'package:academia/constants/common.dart'; export 'package:ionicons/ionicons.dart'; export 'package:academia/widgets/academia_app_bar.dart'; @@ -53,6 +54,7 @@ export 'package:academia/pages/tasks_pages/edittask_page.dart'; export 'package:academia/pages/tasks_pages/newtask_page.dart'; export 'package:academia/pages/tasks_pages/taskinfo_page.dart'; export 'package:magnet/src/magnet_utils.dart'; +export 'package:magnet/src/magnet_exams.dart'; export 'package:academia/tools/time_line/time_line_page.dart'; export 'package:academia/tools/stories/notifications_story_page.dart'; export 'package:academia/controllers/controllers.dart'; diff --git a/lib/main.dart b/lib/main.dart index fdbfe72..ce65d2b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ void main() async { Hive.registerAdapter(ScheduleAdapter()); Hive.registerAdapter(CoursesAdapter()); Hive.registerAdapter(TaskAdapter()); + Hive.registerAdapter(ExamAdapter()); appDB = await Hive.openBox(dbName); // Init settings controller diff --git a/lib/models/exam.dart b/lib/models/exam.dart new file mode 100644 index 0000000..f4a35b7 --- /dev/null +++ b/lib/models/exam.dart @@ -0,0 +1,66 @@ +import 'package:hive/hive.dart'; + +part 'exam.g.dart'; // This file will be generated by the build_runner + +@HiveType(typeId: 4) +class Exam { + @HiveField(0) + String courseCode; + + @HiveField(1) + String day; + + @HiveField(2) + String time; + + @HiveField(3) + String venue; + + @HiveField(4) + String hrs; + + @HiveField(5) + String? invigilator; + + @HiveField(6) + String? coordinator; + + @HiveField(7) + String? campus; + + Exam({ + required this.courseCode, + required this.day, + required this.time, + required this.venue, + required this.hrs, + this.invigilator, + this.coordinator, + this.campus, + }); + + // Factory constructor to create an Exam object from a Map + factory Exam.fromJson(Map json) { + return Exam( + courseCode: json['course_code'], + day: json['day'], + time: json['time'], + venue: json['venue'], + hrs: json['hrs'], + invigilator: json['invigilator'], + coordinator: json['coordinator'], + campus: json['campus'], + ); + } + + // Method to convert an Exam object into a Map + Map toJson() { + return { + 'course_code': courseCode, + 'day': day, + 'time': time, + 'venue': venue, + 'hrs': hrs, + }; + } +} diff --git a/lib/models/exam.g.dart b/lib/models/exam.g.dart new file mode 100644 index 0000000..1a7291f --- /dev/null +++ b/lib/models/exam.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'exam.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ExamAdapter extends TypeAdapter { + @override + final int typeId = 4; + + @override + Exam read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Exam( + courseCode: fields[0] as String, + day: fields[1] as String, + time: fields[2] as String, + venue: fields[3] as String, + hrs: fields[4] as String, + invigilator: fields[5] as String?, + coordinator: fields[6] as String?, + campus: fields[7] as String?, + ); + } + + @override + void write(BinaryWriter writer, Exam obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.courseCode) + ..writeByte(1) + ..write(obj.day) + ..writeByte(2) + ..write(obj.time) + ..writeByte(3) + ..write(obj.venue) + ..writeByte(4) + ..write(obj.hrs) + ..writeByte(5) + ..write(obj.invigilator) + ..writeByte(6) + ..write(obj.coordinator) + ..writeByte(7) + ..write(obj.campus); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ExamAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/tools/exam_timetable/exams_timetable_controller.dart b/lib/tools/exam_timetable/exams_timetable_controller.dart index 53d3caa..c0723e5 100644 --- a/lib/tools/exam_timetable/exams_timetable_controller.dart +++ b/lib/tools/exam_timetable/exams_timetable_controller.dart @@ -1,9 +1,12 @@ import 'package:academia/exports/barrel.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; class ExamsTimeTableController extends GetxController { var index = (-1).obs; + var hasExams = false.obs; late List> quotes = []; + List exams = []; Future fetchRandomQuote() async { try { @@ -35,23 +38,59 @@ class ExamsTimeTableController extends GetxController { } } - Future fetchExams() async { - await Future.delayed(const Duration(seconds: 10)); - return; + Future> fetchExams(List units) async { + exams = (await magnet.fetchExam(units)) + .map((e) => Exam.fromJson(e)) + .toList() + .cast(); + + exams.sort((a, b) { + final formatter = DateFormat('EEEE dd/MM/yy'); + final aDate = formatter.parse(a.day.title()); + final bDate = formatter.parse(b.day.title()); + + // Compare the dates first + final dateComparison = aDate.compareTo(bDate); + if (dateComparison != 0) return dateComparison; + + // If the dates are the same, compare the times + final aTimeRange = a.time.split('-'); + final bTimeRange = b.time.split('-'); + final aStartTime = DateFormat('h:mma').parse(aTimeRange[0]); + final bStartTime = DateFormat('h:mma').parse(bTimeRange[0]); + + return aStartTime.compareTo(bStartTime); + }); + + await appDB.put("exams", exams); + + return exams; } - List userCourses = []; @override Future onInit() async { await fetchRandomQuote(); - // Load user courses - debugPrint(appDB.keys.toString()); - final List coursesData = await appDB.get("timetable"); - coursesData.forEach((element) { - if (element != null) { - userCourses.add(element); - } - }); + hasExams.value = false; + + // Check if the local database has exams + if (appDB.containsKey("exams")) { + hasExams.value = true; + // load the exams + exams = await appDB.get("exams").toList().cast(); + } else { + // load the units + final List courses = + await appDB.get("timetable").toList().cast(); + List courseTitles = courses + .map((e) => + "${e.name?.replaceAll('-', '')}${e.section?.split('-')[0]}") + .toList(); + print(courseTitles); + + // fetch from server + exams = await fetchExams(courseTitles); + hasExams.value = exams.isNotEmpty; + } super.onInit(); } } diff --git a/lib/tools/exam_timetable/exams_timetable_page.dart b/lib/tools/exam_timetable/exams_timetable_page.dart index 72df6a7..b08688f 100644 --- a/lib/tools/exam_timetable/exams_timetable_page.dart +++ b/lib/tools/exam_timetable/exams_timetable_page.dart @@ -1,7 +1,10 @@ import 'package:academia/exports/barrel.dart'; import 'package:academia/tools/exam_timetable/exams_timetable_controller.dart'; +import 'package:academia/tools/exam_timetable/widgets/exam_card.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'widgets/count_down_widget.dart'; class ExamTimeTablePage extends StatefulWidget { const ExamTimeTablePage({super.key}); @@ -28,8 +31,15 @@ class _ExamTimeTablePageState extends State { ), actions: [ IconButton( - onPressed: () {}, - icon: Icon(Ionicons.help), + onPressed: () { + Get.defaultDialog( + title: "Academia Help", + content: const Text( + "Never miss an exam again with the intuitive exam timetable", + ), + ); + }, + icon: const Icon(Ionicons.help), ), ], expandedHeight: 250, @@ -40,15 +50,8 @@ class _ExamTimeTablePageState extends State { ), padding: const EdgeInsets.all(12), child: GestureDetector( - onPanUpdate: ((details) { - if (details.delta.dx > 0) { - controller.nextQuote(); - } - - if (details.delta.dx < 0) { - controller.previousQuote(); - } - }), + onTap: () => controller.previousQuote(), + onDoubleTap: () => controller.nextQuote(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -58,7 +61,7 @@ class _ExamTimeTablePageState extends State { () => Text( controller.index > -1 ? controller.quotes[controller.index.value]["q"] - : "So long as we are being remembered, we remain alive.", + : "Fool me once shame on you; fool me twice, shame on me.", textAlign: TextAlign.center, style: Theme.of(context) .textTheme @@ -72,7 +75,7 @@ class _ExamTimeTablePageState extends State { () => Text( controller.index > -1 ? controller.quotes[controller.index.value]["a"] - : "Carlos Ruiz Zafon", + : "Chinese Proverb", textAlign: TextAlign.end, ), ), @@ -82,21 +85,43 @@ class _ExamTimeTablePageState extends State { ), ), ), + SliverToBoxAdapter( + child: Obx( + () => controller.hasExams.value + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: ExamCountDownWidget( + examCount: controller.exams.length, + exam: controller.exams[0], + endtime: getExamDate(controller.exams[0]), + ), + ) + : const SizedBox(), + ), + ), SliverFillRemaining( hasScrollBody: true, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Seems you have no exams at the moment", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - fontFamily: GoogleFonts.figtree().fontFamily, + child: Obx( + () => controller.hasExams.value + ? ListView.builder( + itemCount: controller.exams.length, + itemBuilder: (context, index) => + ExamCard(exam: controller.exams[index]), + ) + : Center( + child: Text( + "Seems you have no exams at the moment 🤞", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), ), - ), - ], + ), ), ), ) @@ -105,7 +130,6 @@ class _ExamTimeTablePageState extends State { floatingActionButton: FloatingActionButton( onPressed: () { showModalBottomSheet( - isDismissible: false, context: context, builder: (context) => StatefulBuilder( builder: (context, StateSetter setState) { @@ -135,7 +159,6 @@ class _ExamTimeTablePageState extends State { ); return; } - print("Here"); if (mounted) { setState(() { _isSearching = true; @@ -185,9 +208,14 @@ class _ExamTimeTablePageState extends State { ); }, child: const Icon( - Ionicons.add, + Ionicons.search, ), ), ); } + + DateTime getExamDate(Exam exam) { + final formatter = DateFormat('EEEE dd/MM/yy'); + return formatter.parse(exam.day.title()); + } } diff --git a/lib/tools/exam_timetable/widgets/count_down_widget.dart b/lib/tools/exam_timetable/widgets/count_down_widget.dart new file mode 100644 index 0000000..8a49c85 --- /dev/null +++ b/lib/tools/exam_timetable/widgets/count_down_widget.dart @@ -0,0 +1,92 @@ +import 'package:academia/exports/barrel.dart'; +import 'package:academia/tools/exam_timetable/widgets/exam_card.dart'; +import 'package:flutter_timer_countdown/flutter_timer_countdown.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ExamCountDownWidget extends StatefulWidget { + const ExamCountDownWidget({ + super.key, + required this.endtime, + required this.examCount, + this.exam, + }); + final DateTime endtime; + final int examCount; + final Exam? exam; + + @override + State createState() => _ExamCountDownWidgetState(); +} + +class _ExamCountDownWidgetState extends State { + var isCritical = false; + @override + Widget build(BuildContext context) { + return FlutterCarousel( + items: [ + Container( + decoration: BoxDecoration( + color: isCritical + ? Theme.of(context).colorScheme.errorContainer + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8)), + width: MediaQuery.of(context).size.width * 0.8, + child: Center( + child: TimerCountdown( + timeTextStyle: + Theme.of(context).textTheme.displayMedium!.copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + format: CountDownTimerFormat.daysHoursMinutesSeconds, + endTime: widget.endtime, + onTick: ((remainingTime) { + if (remainingTime.inMinutes < 5) { + setState(() { + isCritical = true; + }); + } + }), + ), + ), + ), + widget.exam == null + ? Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8)), + width: MediaQuery.of(context).size.width * 0.8, + child: Center( + child: Text( + "Aah sh*t here we go again", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + ), + ), + ) + : ExamCard(exam: widget.exam!), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8)), + width: MediaQuery.of(context).size.width * 0.8, + child: Center( + child: Text( + "${widget.examCount} exams left to go 🫡", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontFamily: GoogleFonts.figtree().fontFamily, + ), + ), + ), + ) + ], + options: CarouselOptions( + height: 200, + showIndicator: true, + autoPlay: true, + slideIndicator: const CircularSlideIndicator(itemSpacing: 14)), + ); + } +} diff --git a/lib/tools/exam_timetable/widgets/exam_card.dart b/lib/tools/exam_timetable/widgets/exam_card.dart new file mode 100644 index 0000000..4d257a0 --- /dev/null +++ b/lib/tools/exam_timetable/widgets/exam_card.dart @@ -0,0 +1,72 @@ +import 'package:academia/exports/barrel.dart'; + +class ExamCard extends StatelessWidget { + const ExamCard({super.key, required this.exam, this.ispast = false}); + final Exam exam; + final bool ispast; + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: ListTile( + enabled: !ispast, + leading: const Icon(Ionicons.ribbon), + title: Text( + exam.courseCode.toString(), + ), + subtitle: Column( + children: [ + const SizedBox(height: 12), + Row( + children: [ + const Icon(Ionicons.calendar), + const SizedBox(width: 4), + Text( + exam.day, + ) + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Ionicons.location), + const SizedBox(width: 4), + Text( + exam.venue, + ), + const Spacer(), + const Icon(Ionicons.time), + const SizedBox(width: 2), + Text( + exam.time, + ), + ], + ), + const SizedBox(height: 12), + exam.coordinator != null && exam.invigilator != null + ? Row(children: [ + const Icon(Ionicons.person_circle), + const SizedBox(width: 4), + Text( + exam.coordinator.toString(), + ), + const Spacer(), + const Icon(Ionicons.people_circle), + const SizedBox(width: 4), + Text( + exam.coordinator.toString(), + ) + ]) + : const SizedBox() + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 88da6bf..6ad3579 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: story_view: ^0.16.0 ionicons: ^0.2.2 google_fonts: ^6.2.1 + flutter_timer_countdown: ^1.0.7 From 1a0624de0ecbb131bc1c1e50bf214a93b67e64e3 Mon Sep 17 00:00:00 2001 From: Erick Date: Sat, 6 Apr 2024 19:20:49 +0300 Subject: [PATCH 4/4] feat: exam timetable done! --- lib/constants/tools.dart | 23 +++--- .../exams_timetable_controller.dart | 47 ++++++++++- .../exam_timetable/exams_timetable_page.dart | 78 ++++++++++++++++--- .../widgets/count_down_widget.dart | 2 +- 4 files changed, 127 insertions(+), 23 deletions(-) diff --git a/lib/constants/tools.dart b/lib/constants/tools.dart index 0afab46..f4a130d 100644 --- a/lib/constants/tools.dart +++ b/lib/constants/tools.dart @@ -2,6 +2,18 @@ import 'package:get/get.dart'; import 'package:academia/exports/barrel.dart'; final List> allTools = [ + { + "id": 8, + "name": "Exam Timetable", + "action": "Show exam timetable", + "image": "assets/images/exam_timetable.png", + "ontap": () { + Get.to(const ExamTimeTablePage()); + }, + "description": + "Exams around the corner? Don't panic we've got you covered with the timetable", + }, + { "id": 1, "name": "GPA Calculator", @@ -139,17 +151,6 @@ final List> allTools = [ "description": "Curious to know how many classes you have missed this semester, this might be the tool", }, - { - "id": 8, - "name": "Exam Timetable", - "action": "Show exam timetable", - "image": "assets/images/exam_timetable.png", - "ontap": () { - Get.to(const ExamTimeTablePage()); - }, - "description": - "Exams around the corner? Don't panic we've got you covered with the timetable", - }, { "id": 9, "name": "Task Manager", diff --git a/lib/tools/exam_timetable/exams_timetable_controller.dart b/lib/tools/exam_timetable/exams_timetable_controller.dart index c0723e5..ee41fb9 100644 --- a/lib/tools/exam_timetable/exams_timetable_controller.dart +++ b/lib/tools/exam_timetable/exams_timetable_controller.dart @@ -39,11 +39,35 @@ class ExamsTimeTableController extends GetxController { } Future> fetchExams(List units) async { - exams = (await magnet.fetchExam(units)) + final examData = (await magnet.fetchExam(units)) .map((e) => Exam.fromJson(e)) .toList() .cast(); + examData.sort((a, b) { + final formatter = DateFormat('EEEE dd/MM/yy'); + final aDate = formatter.parse(a.day.title()); + final bDate = formatter.parse(b.day.title()); + + // Compare the dates first + final dateComparison = aDate.compareTo(bDate); + if (dateComparison != 0) return dateComparison; + + // If the dates are the same, compare the times + final aTimeRange = a.time.split('-'); + final bTimeRange = b.time.split('-'); + final aStartTime = DateFormat('h:mma').parse(aTimeRange[0]); + final bStartTime = DateFormat('h:mma').parse(bTimeRange[0]); + + return aStartTime.compareTo(bStartTime); + }); + + return examData; + } + + Future addExamToStorage(Exam exam) async { + exams = await appDB.get("exams").toList().cast(); + exams.add(exam); exams.sort((a, b) { final formatter = DateFormat('EEEE dd/MM/yy'); final aDate = formatter.parse(a.day.title()); @@ -64,7 +88,25 @@ class ExamsTimeTableController extends GetxController { await appDB.put("exams", exams); - return exams; + // trigger a data refersh + hasExams.value = false; + hasExams.value = true; + } + + Future removeExamFromStorage(Exam exam) async { + exams = await appDB.get("exams").toList().cast(); + exams.remove(exam); + + if (exams.isEmpty) { + await appDB.delete("exams"); + // trigger a data refersh + hasExams.value = false; + } else { + await appDB.put("exams", exams); + // trigger a data refersh + hasExams.value = false; + hasExams.value = true; + } } @override @@ -89,6 +131,7 @@ class ExamsTimeTableController extends GetxController { // fetch from server exams = await fetchExams(courseTitles); + await appDB.put("exams", exams); hasExams.value = exams.isNotEmpty; } super.onInit(); diff --git a/lib/tools/exam_timetable/exams_timetable_page.dart b/lib/tools/exam_timetable/exams_timetable_page.dart index b08688f..5953122 100644 --- a/lib/tools/exam_timetable/exams_timetable_page.dart +++ b/lib/tools/exam_timetable/exams_timetable_page.dart @@ -19,6 +19,8 @@ class _ExamTimeTablePageState extends State { final controller = Get.put(ExamsTimeTableController()); final _searchController = TextEditingController(); bool _isSearching = false; + bool _searchComplete = false; + List searchedExams = []; @override Widget build(BuildContext context) { @@ -26,6 +28,9 @@ class _ExamTimeTablePageState extends State { body: CustomScrollView( slivers: [ SliverAppBar( + floating: true, + snap: true, + pinned: true, title: Text( "Hi ${userController.user.value!.name!.split(' ')[0].title()}", ), @@ -35,7 +40,7 @@ class _ExamTimeTablePageState extends State { Get.defaultDialog( title: "Academia Help", content: const Text( - "Never miss an exam again with the intuitive exam timetable", + "Never miss an exam again with the intuitive exam timetable\nDouble tap an exam card to remove it", ), ); }, @@ -46,7 +51,7 @@ class _ExamTimeTablePageState extends State { flexibleSpace: FlexibleSpaceBar( background: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.tertiaryContainer, ), padding: const EdgeInsets.all(12), child: GestureDetector( @@ -107,12 +112,17 @@ class _ExamTimeTablePageState extends State { () => controller.hasExams.value ? ListView.builder( itemCount: controller.exams.length, - itemBuilder: (context, index) => - ExamCard(exam: controller.exams[index]), + itemBuilder: (context, index) => GestureDetector( + onDoubleTap: () async { + await controller + .removeExamFromStorage(controller.exams[index]); + }, + child: ExamCard(exam: controller.exams[index]), + ), ) : Center( child: Text( - "Seems you have no exams at the moment 🤞", + "Who dares summon me?🧞", textAlign: TextAlign.center, style: Theme.of(context) .textTheme @@ -162,7 +172,17 @@ class _ExamTimeTablePageState extends State { if (mounted) { setState(() { _isSearching = true; + _searchComplete = false; }); + searchedExams = await controller.fetchExams( + _searchController.text.split(","), + ); + + if (searchedExams.isNotEmpty) { + setState(() { + _searchComplete = true; + }); + } // await examtimetableController.fetchExams(); if (!mounted) { @@ -179,8 +199,7 @@ class _ExamTimeTablePageState extends State { ), const SizedBox(height: 12), _isSearching - ? const Text("Loading") - : Container( + ? Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), @@ -189,7 +208,7 @@ class _ExamTimeTablePageState extends State { .tertiaryContainer, ), child: Text( - "Please input your units seperated with commas and wait for the magic to happen🪄", + "Your wish is my command. Performing forbidden magic 🧞\n Double Tap a course to add it", textAlign: TextAlign.center, style: Theme.of(context) .textTheme @@ -199,7 +218,48 @@ class _ExamTimeTablePageState extends State { GoogleFonts.figtree().fontFamily, ), ), - ), + ) + : _searchComplete + ? SizedBox( + height: + MediaQuery.of(context).size.height * 0.4, + child: ListView.builder( + itemCount: searchedExams.length, + itemBuilder: (context, index) => + GestureDetector( + onDoubleTap: () async { + // Add exam + await controller.addExamToStorage( + searchedExams[index]); + searchedExams + .remove(searchedExams[index]); + setState(() {}); + }, + child: ExamCard( + exam: searchedExams[index], + ), + )), + ) + : Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context) + .colorScheme + .tertiaryContainer, + ), + child: Text( + "Please input your units seperated with commas and let the genie work his forbidden magic!", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + fontFamily: + GoogleFonts.figtree().fontFamily, + ), + ), + ), ], ), ); diff --git a/lib/tools/exam_timetable/widgets/count_down_widget.dart b/lib/tools/exam_timetable/widgets/count_down_widget.dart index 8a49c85..b6d1fcd 100644 --- a/lib/tools/exam_timetable/widgets/count_down_widget.dart +++ b/lib/tools/exam_timetable/widgets/count_down_widget.dart @@ -68,7 +68,7 @@ class _ExamCountDownWidgetState extends State { : ExamCard(exam: widget.exam!), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(8)), width: MediaQuery.of(context).size.width * 0.8, child: Center(