From f39e3e97f605e8ca8d16bde042b5b247133defc7 Mon Sep 17 00:00:00 2001 From: Fareez Iqmal <60868965+iqfareez@users.noreply.github.com> Date: Sun, 2 Apr 2023 20:24:53 +0800 Subject: [PATCH] :tada: Initial impl POC of final exam schedule #66 the subject is taken from saved schedule and the exam's date and time is from albiruni API --- Inno/setup-script.iss | 2 +- lib/isar_models/saved_final_exam.dart | 32 + lib/isar_models/saved_final_exam.g.dart | 1046 +++++++++++++++++++++ lib/model/exam_date_time.dart | 34 + lib/services/isar_service.dart | 1 + lib/util/http_fetcher.dart | 20 + lib/views/body.dart | 12 +- lib/views/final_exam/final_exam_page.dart | 189 ++++ pubspec.yaml | 4 +- 9 files changed, 1336 insertions(+), 4 deletions(-) create mode 100644 lib/isar_models/saved_final_exam.dart create mode 100644 lib/isar_models/saved_final_exam.g.dart create mode 100644 lib/model/exam_date_time.dart create mode 100644 lib/util/http_fetcher.dart create mode 100644 lib/views/final_exam/final_exam_page.dart diff --git a/Inno/setup-script.iss b/Inno/setup-script.iss index e27fe1d..e778c7c 100644 --- a/Inno/setup-script.iss +++ b/Inno/setup-script.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "IIUM Schedule" -#define MyAppVersion "1.1.4.0" +#define MyAppVersion "1.2.0.0" #define MyAppPublisher "Muhammad Fareez Iqmal" #define MyAppURL "https://iiumschedule.iqfareez.com/" #define MyAppExeName "iium_schedule.exe" diff --git a/lib/isar_models/saved_final_exam.dart b/lib/isar_models/saved_final_exam.dart new file mode 100644 index 0000000..72a2dcd --- /dev/null +++ b/lib/isar_models/saved_final_exam.dart @@ -0,0 +1,32 @@ +import 'package:isar/isar.dart'; + +part 'saved_final_exam.g.dart'; + +@collection +class SavedFinalExam { + Id? id; + + /// Subject course code + String courseCode; + + /// Subject name + String courseTitle; + + /// Exam's date and start time + DateTime dateTime; + + /// Candidate's seat number + int seatNumber; + + /// Exam venue + String venue; + + /// Final exam information + SavedFinalExam({ + required this.courseCode, + required this.courseTitle, + required this.dateTime, + required this.seatNumber, + required this.venue, + }); +} diff --git a/lib/isar_models/saved_final_exam.g.dart b/lib/isar_models/saved_final_exam.g.dart new file mode 100644 index 0000000..227390c --- /dev/null +++ b/lib/isar_models/saved_final_exam.g.dart @@ -0,0 +1,1046 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_final_exam.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetSavedFinalExamCollection on Isar { + IsarCollection get savedFinalExams => this.collection(); +} + +const SavedFinalExamSchema = CollectionSchema( + name: r'SavedFinalExam', + id: -5567489116739535725, + properties: { + r'courseCode': PropertySchema( + id: 0, + name: r'courseCode', + type: IsarType.string, + ), + r'courseTitle': PropertySchema( + id: 1, + name: r'courseTitle', + type: IsarType.string, + ), + r'dateTime': PropertySchema( + id: 2, + name: r'dateTime', + type: IsarType.dateTime, + ), + r'seatNumber': PropertySchema( + id: 3, + name: r'seatNumber', + type: IsarType.long, + ), + r'venue': PropertySchema( + id: 4, + name: r'venue', + type: IsarType.string, + ) + }, + estimateSize: _savedFinalExamEstimateSize, + serialize: _savedFinalExamSerialize, + deserialize: _savedFinalExamDeserialize, + deserializeProp: _savedFinalExamDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _savedFinalExamGetId, + getLinks: _savedFinalExamGetLinks, + attach: _savedFinalExamAttach, + version: '3.0.5', +); + +int _savedFinalExamEstimateSize( + SavedFinalExam object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.courseCode.length * 3; + bytesCount += 3 + object.courseTitle.length * 3; + bytesCount += 3 + object.venue.length * 3; + return bytesCount; +} + +void _savedFinalExamSerialize( + SavedFinalExam object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.courseCode); + writer.writeString(offsets[1], object.courseTitle); + writer.writeDateTime(offsets[2], object.dateTime); + writer.writeLong(offsets[3], object.seatNumber); + writer.writeString(offsets[4], object.venue); +} + +SavedFinalExam _savedFinalExamDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SavedFinalExam( + courseCode: reader.readString(offsets[0]), + courseTitle: reader.readString(offsets[1]), + dateTime: reader.readDateTime(offsets[2]), + seatNumber: reader.readLong(offsets[3]), + venue: reader.readString(offsets[4]), + ); + object.id = id; + return object; +} + +P _savedFinalExamDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readDateTime(offset)) as P; + case 3: + return (reader.readLong(offset)) as P; + case 4: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _savedFinalExamGetId(SavedFinalExam object) { + return object.id ?? Isar.autoIncrement; +} + +List> _savedFinalExamGetLinks(SavedFinalExam object) { + return []; +} + +void _savedFinalExamAttach( + IsarCollection col, Id id, SavedFinalExam object) { + object.id = id; +} + +extension SavedFinalExamQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SavedFinalExamQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension SavedFinalExamQueryFilter + on QueryBuilder { + QueryBuilder + courseCodeEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'courseCode', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'courseCode', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'courseCode', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseCodeIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'courseCode', + value: '', + )); + }); + } + + QueryBuilder + courseCodeIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'courseCode', + value: '', + )); + }); + } + + QueryBuilder + courseTitleEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'courseTitle', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'courseTitle', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'courseTitle', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + courseTitleIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'courseTitle', + value: '', + )); + }); + } + + QueryBuilder + courseTitleIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'courseTitle', + value: '', + )); + }); + } + + QueryBuilder + dateTimeEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'dateTime', + value: value, + )); + }); + } + + QueryBuilder + dateTimeGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'dateTime', + value: value, + )); + }); + } + + QueryBuilder + dateTimeLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'dateTime', + value: value, + )); + }); + } + + QueryBuilder + dateTimeBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'dateTime', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + idIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'id', + )); + }); + } + + QueryBuilder + idIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'id', + )); + }); + } + + QueryBuilder idEqualTo( + Id? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id? lower, + Id? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + seatNumberEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'seatNumber', + value: value, + )); + }); + } + + QueryBuilder + seatNumberGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'seatNumber', + value: value, + )); + }); + } + + QueryBuilder + seatNumberLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'seatNumber', + value: value, + )); + }); + } + + QueryBuilder + seatNumberBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'seatNumber', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + venueEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'venue', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'venue', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'venue', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + venueIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'venue', + value: '', + )); + }); + } + + QueryBuilder + venueIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'venue', + value: '', + )); + }); + } +} + +extension SavedFinalExamQueryObject + on QueryBuilder {} + +extension SavedFinalExamQueryLinks + on QueryBuilder {} + +extension SavedFinalExamQuerySortBy + on QueryBuilder { + QueryBuilder + sortByCourseCode() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseCode', Sort.asc); + }); + } + + QueryBuilder + sortByCourseCodeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseCode', Sort.desc); + }); + } + + QueryBuilder + sortByCourseTitle() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseTitle', Sort.asc); + }); + } + + QueryBuilder + sortByCourseTitleDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseTitle', Sort.desc); + }); + } + + QueryBuilder sortByDateTime() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'dateTime', Sort.asc); + }); + } + + QueryBuilder + sortByDateTimeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'dateTime', Sort.desc); + }); + } + + QueryBuilder + sortBySeatNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'seatNumber', Sort.asc); + }); + } + + QueryBuilder + sortBySeatNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'seatNumber', Sort.desc); + }); + } + + QueryBuilder sortByVenue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'venue', Sort.asc); + }); + } + + QueryBuilder sortByVenueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'venue', Sort.desc); + }); + } +} + +extension SavedFinalExamQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByCourseCode() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseCode', Sort.asc); + }); + } + + QueryBuilder + thenByCourseCodeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseCode', Sort.desc); + }); + } + + QueryBuilder + thenByCourseTitle() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseTitle', Sort.asc); + }); + } + + QueryBuilder + thenByCourseTitleDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'courseTitle', Sort.desc); + }); + } + + QueryBuilder thenByDateTime() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'dateTime', Sort.asc); + }); + } + + QueryBuilder + thenByDateTimeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'dateTime', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenBySeatNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'seatNumber', Sort.asc); + }); + } + + QueryBuilder + thenBySeatNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'seatNumber', Sort.desc); + }); + } + + QueryBuilder thenByVenue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'venue', Sort.asc); + }); + } + + QueryBuilder thenByVenueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'venue', Sort.desc); + }); + } +} + +extension SavedFinalExamQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByCourseCode( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'courseCode', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByCourseTitle( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'courseTitle', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByDateTime() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'dateTime'); + }); + } + + QueryBuilder + distinctBySeatNumber() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'seatNumber'); + }); + } + + QueryBuilder distinctByVenue( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'venue', caseSensitive: caseSensitive); + }); + } +} + +extension SavedFinalExamQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder courseCodeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'courseCode'); + }); + } + + QueryBuilder courseTitleProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'courseTitle'); + }); + } + + QueryBuilder dateTimeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'dateTime'); + }); + } + + QueryBuilder seatNumberProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'seatNumber'); + }); + } + + QueryBuilder venueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'venue'); + }); + } +} diff --git a/lib/model/exam_date_time.dart b/lib/model/exam_date_time.dart new file mode 100644 index 0000000..8704120 --- /dev/null +++ b/lib/model/exam_date_time.dart @@ -0,0 +1,34 @@ +import 'package:recase/recase.dart'; + +class ExamDateTime { + String? date; + String? time; + + ExamDateTime({this.date, this.time}); + + ExamDateTime.fromJson(Map json) { + // Month need to be converted from 'JAN' to 'Jan' + // otherwise it will throw format exception + // ignore: no_leading_underscores_for_local_identifiers + var _date = json["date"]; + var startIndex = _date.indexOf('-'); + var month = + ReCase(_date.substring(startIndex + 1, startIndex + 4)).titleCase; + // replace month to new month + _date = _date.replaceAll(month.toUpperCase(), month); + date = _date; + time = json["time"]; + } + + Map toJson() { + final Map data = {}; + data["date"] = date; + data["time"] = time; + return data; + } + + @override + String toString() { + return '$date $time'; + } +} diff --git a/lib/services/isar_service.dart b/lib/services/isar_service.dart index 3a0c140..4922a81 100644 --- a/lib/services/isar_service.dart +++ b/lib/services/isar_service.dart @@ -63,6 +63,7 @@ class IsarService { yield* isar.savedSchedules.watchObject(id, fireImmediately: true); } + Future getSavedSchedule({required int id}) async { final isar = await db; diff --git a/lib/util/http_fetcher.dart b/lib/util/http_fetcher.dart new file mode 100644 index 0000000..932aeb9 --- /dev/null +++ b/lib/util/http_fetcher.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../model/exam_date_time.dart'; + +class HttpFetcher { + /// Fetch the final exam date and time. It is sorted already I think due to how it + /// fetched by the API server + static Future fetchExam( + String courseCode, String session, int semester) async { + var response = await http.get(Uri.parse( + 'https://albiruni.up.railway.app/exams/$courseCode?sesssion=$session&semester=$semester')); + if (response.statusCode == 200) { + return ExamDateTime.fromJson(jsonDecode(response.body)); + } else { + throw Exception('Failed to load data'); + } + } +} diff --git a/lib/views/body.dart b/lib/views/body.dart index adb38eb..06b64c6 100644 --- a/lib/views/body.dart +++ b/lib/views/body.dart @@ -15,6 +15,7 @@ import '../util/launcher_url.dart'; import '../util/my_ftoast.dart'; import 'check_update_page.dart'; import 'course browser/browser.dart'; +import 'final_exam/final_exam_page.dart'; import 'saved_schedule/saved_schedule_layout.dart'; import 'scheduler/schedule_maker_entry.dart'; import 'settings_page.dart'; @@ -29,6 +30,8 @@ class MyBody extends StatefulWidget { class _MyBodyState extends State { final IsarService _isarService = IsarService(); final GlobalKey _listKey = GlobalKey(); + + // page index (wether it is scheduke of course browser) int selectedIndex = 0; @override @@ -168,6 +171,14 @@ class _MyBodyState extends State { ), ), const SizedBox(height: 20.0), + // HACK: Of course, this is not the actual location + // of final exam button would be + OutlinedButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const FinalExamPage())); + }, + child: const Text('Open final exam')), StreamBuilder( stream: _isarService.listenToAllSchedulesChanges(), @@ -256,7 +267,6 @@ class _MyBodyState extends State { onPressed: () async { await Navigator.of(context).push( CupertinoPageRoute(builder: (_) => ScheduleMakerEntry())); - // setState(() {}); }, icon: const Icon(Icons.add), label: const Text('Create'), diff --git a/lib/views/final_exam/final_exam_page.dart b/lib/views/final_exam/final_exam_page.dart new file mode 100644 index 0000000..bba3f9d --- /dev/null +++ b/lib/views/final_exam/final_exam_page.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../model/exam_date_time.dart'; +import '../../services/isar_service.dart'; +import '../../util/http_fetcher.dart'; + +class FinalExamPage extends StatefulWidget { + const FinalExamPage({super.key}); + + @override + State createState() => _FinalExamPageState(); +} + +class _FinalExamPageState extends State { + // TODO: add banner please bring along matric card and exam slip + List? courseCodes; + @override + Widget build(BuildContext context) { + print(courseCodes); + return Scaffold( + appBar: AppBar( + title: const Text('Final Exam'), + actions: [ + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'add-exam': + print('add exam'); + break; + case 'import-exams': + showDialog( + context: context, + builder: (_) { + final IsarService isarService = IsarService(); + var schedules = isarService.getAllSchedule(); + return SimpleDialog( + children: [ + SimpleDialogOption( + onPressed: () {}, + child: const Text("Import from I-Ma'Luum"), + ), + const Divider(), + if (schedules.isEmpty) + const SimpleDialogOption( + child: Text('No saved schedule to import'), + ), + for (var schedule in schedules) + SimpleDialogOption( + onPressed: () async { + schedule.subjects.loadSync(); + + setState(() { + courseCodes = schedule.subjects + .map((e) => e.code) + .toList(); + }); + Navigator.pop(context); + }, + child: Text('Import from ${schedule.title!}'), + ), + ], + ); + }); + break; + default: + } + }, + icon: const Icon(Icons.add), + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'add-exam', + child: Text('Add exam'), + ), + const PopupMenuItem( + value: 'import-exams', + child: Text('Import exams'), + ), + ], + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + elevation: 0, + // color: Colors.black, + color: Theme.of(context).colorScheme.primaryContainer, + clipBehavior: Clip.hardEdge, + shape: const ContinuousRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(50.0), + ), + ), + child: InkWell( + onTap: () {}, + child: SizedBox( + height: 100, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Upcoming exam (In 1 day)', + style: TextStyle(fontWeight: FontWeight.w100), + ), + const SizedBox(height: 2), + Text( + "MANU 4313", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + fontSize: 20.0, + fontWeight: FontWeight.bold), + ), + const Text( + '9.00 am', + // style: TextStyle(fontWeight: FontWeight.w100), + ), + ], + ), + ), + // TODO: This banner is just mock as for now + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + children: [ + const Icon(Icons.chair_alt), + const Text('194'), + ], + ), + Row( + children: [ + const Icon(Icons.location_on_outlined), + const Text('Main Audi 3'), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 5), + if (courseCodes != null) + for (var courseCode in courseCodes!) + FutureBuilder( + future: HttpFetcher.fetchExam(courseCode, '2022/2023', 1), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const ListTile( + leading: SizedBox( + height: 30, + width: 30, + child: CircularProgressIndicator()), + ); + } + if (snapshot.hasError) { + courseCodes!.remove(courseCode); + return const SizedBox.shrink(); + } + + final format = DateFormat('dd-MMM-yy h.mm a'); + final dateTime = format.parse(snapshot.data!.toString()); + + return ListTile( + title: Text(courseCode.toUpperCase()), + subtitle: Text( + DateFormat('EEE, d MMMM yyyy').format(dateTime)), + ); + }), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 6228905..9ec5451 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # When changing this version, also change the versions in # - msix block (msix_version) # - Inno setup (MyAppVersion) -version: 1.1.4+26 +version: 1.2.0-pre.1+27 environment: sdk: ">=2.17.0 <3.0.0" @@ -114,7 +114,7 @@ msix_config: display_name: IIUM Schedule publisher_display_name: Muhammad Fareez Iqmal identity_name: fareez.flutter.iiumschedule - msix_version: 1.1.4.0 # same as inno setup (MyAppVersion) + msix_version: 1.2.0.0 # same as inno setup (MyAppVersion) logo_path: ".\\assets\\logo\\app-logo.png" capabilities: "internetClient" certificate_path: ".\\windows\\CERTIFICATE.pfx"