diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 051ac1bda..f32f1a4a2 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,9 +1,11 @@ on: push: - branches: [ master, develop ] + branches: [master, develop] jobs: bump_version: + permissions: + actions: "write" name: "Bump pubspec version" runs-on: ubuntu-latest env: @@ -14,12 +16,19 @@ jobs: with: token: ${{ secrets.NIAEFEUPBOT_PAT }} + - name: Get develop hash + if: github.ref == 'refs/heads/master' + run: | + git fetch origin develop + current_hash=$(git rev-parse origin/develop) + echo "DEVELOPHASH=$current_hash" >> $GITHUB_ENV + - name: Bump flutter patch version - if: github.ref == 'refs/heads/develop' + if: github.ref == 'refs/heads/develop' || github.sha != env.DEVELOPHASH run: perl -i -pe 's/^(\d+\.\d+\.)(\d+)(\+)(\d+)$/$1.($2+1).($3).($4+1)/e' ${{ env.APP_VERSION_PATH }} - name: Bump flutter minor version - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' && github.sha == env.DEVELOPHASH run: perl -i -pe 's/^(\d+)(\.)(\d+)(\.)(\d+)(\+)(\d+)$/$1.($2).($3+1).($4).(0).($6).($7+1)/e' ${{ env.APP_VERSION_PATH }} - name: Copy app version to pubspec @@ -29,9 +38,16 @@ jobs: with: commit_message: "Bump app version [no ci]" - - name: Propagate master version bump to develop + - name: Propagate master version bump to develop if master version is aad if: github.ref == 'refs/heads/master' - run: git push --force-with-lease origin HEAD:develop + run: | + git fetch origin develop + if [[ $(git diff --quiet HEAD~1 origin/develop) ]]; then + echo "Master version does not match develop version" + else + echo "Master version matches develop version" + git push --force-with-lease origin HEAD:develop + fi build: name: "Build App Bundle" @@ -49,7 +65,7 @@ jobs: - uses: actions/setup-java@v3 with: java-version: ${{env.JAVA_VERSION}} - distribution: 'zulu' + distribution: "zulu" - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} diff --git a/uni/app_version.txt b/uni/app_version.txt index 3a04413b4..b7cdb1a37 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.7.0+190 \ No newline at end of file +1.7.9+199 \ No newline at end of file diff --git a/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart index c6dbdc8e2..f0d87293b 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart @@ -8,8 +8,9 @@ import 'package:uni/model/entities/session.dart'; class AllCourseUnitsFetcher { Future?> getAllCourseUnitsAndCourseAverages( List courses, - Session session, - ) async { + Session session, { + List? currentCourseUnits, + }) async { final allCourseUnits = []; for (final course in courses) { @@ -17,6 +18,7 @@ class AllCourseUnitsFetcher { final courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( course, session, + currentCourseUnits: currentCourseUnits, ); allCourseUnits.addAll(courseUnits.where((c) => c.enrollmentIsValid())); } catch (e) { @@ -30,8 +32,9 @@ class AllCourseUnitsFetcher { Future> _getAllCourseUnitsAndCourseAveragesFromCourse( Course course, - Session session, - ) async { + Session session, { + List? currentCourseUnits, + }) async { final url = '${NetworkRouter.getBaseUrl(course.faculty!)}' 'fest_geral.curso_percurso_academico_view'; final response = await NetworkRouter.getWithCookies( @@ -41,6 +44,10 @@ class AllCourseUnitsFetcher { }, session, ); - return parseCourseUnitsAndCourseAverage(response, course); + return parseCourseUnitsAndCourseAverage( + response, + course, + currentCourseUnits: currentCourseUnits, + ); } } diff --git a/uni/lib/controller/fetchers/departures_fetcher.dart b/uni/lib/controller/fetchers/departures_fetcher.dart index 921c42113..64e2d3585 100644 --- a/uni/lib/controller/fetchers/departures_fetcher.dart +++ b/uni/lib/controller/fetchers/departures_fetcher.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'dart:io'; import 'package:html/dom.dart'; import 'package:html/parser.dart'; -import 'package:http/http.dart' as http; +import 'package:http/io_client.dart' as http; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/bus.dart'; import 'package:uni/model/entities/bus_stop.dart'; @@ -13,12 +14,17 @@ class DeparturesFetcher { final String _stopCode; final BusStopData _stopData; + static final _client = http.IOClient( + HttpClient(context: SecurityContext()) + ..badCertificateCallback = + (cert, host, port) => host == 'www.stcp.pt' && port == 443, + ); Future _getCSRFToken() async { final url = 'https://www.stcp.pt/en/travel/timetables/?paragem=$_stopCode&t=smsbus'; - final response = await http.get(url.toUri()); + final response = await _client.get(url.toUri()); final htmlResponse = parse(response.body); final scriptText = htmlResponse @@ -50,7 +56,7 @@ class DeparturesFetcher { final url = 'https://www.stcp.pt/pt/itinerarium/soapclient.php?codigo=$_stopCode&hash123=$csrfToken'; - final response = await http.get(url.toUri()); + final response = await _client.get(url.toUri()); final htmlResponse = parse(response.body); final tableEntries = @@ -111,7 +117,8 @@ class DeparturesFetcher { // Search by approximate name final url = 'https://www.stcp.pt/pt/itinerarium/callservice.php?action=srchstoplines&stopname=$stopCode'; - final response = await http.post(url.toUri()); + + final response = await _client.post(url.toUri()); final json = jsonDecode(response.body) as List; for (final busKey in json) { final bus = busKey as Map; @@ -134,7 +141,8 @@ class DeparturesFetcher { static Future> getBusesStoppingAt(String stop) async { final url = 'https://www.stcp.pt/pt/itinerarium/callservice.php?action=srchstoplines&stopcode=$stop'; - final response = await http.post(url.toUri()); + + final response = await _client.post(url.toUri()); final json = jsonDecode(response.body) as List; diff --git a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart index 6d9594a4b..e7e8ea77e 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart @@ -1,4 +1,5 @@ import 'package:http/http.dart'; +import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_schedule_html.dart'; @@ -20,10 +21,13 @@ class ScheduleFetcherHtml extends ScheduleFetcher { @override Future> getLectures(Session session, Profile profile) async { final dates = getDates(); - final urls = getEndpoints(session); - final lectureResponses = []; - for (final course in profile.courses) { - for (final url in urls) { + final baseUrls = NetworkRouter.getBaseUrlsFromSession(session); + + final lectureResponses = >[]; + for (final baseUrl in baseUrls) { + final url = '${baseUrl}hor_geral.estudantes_view'; + + for (final course in profile.courses) { final response = await NetworkRouter.getWithCookies( url, { @@ -34,13 +38,14 @@ class ScheduleFetcherHtml extends ScheduleFetcher { }, session, ); - lectureResponses.add(response); + lectureResponses.add(Tuple2(response, baseUrl)); } } final lectures = await Future.wait( - lectureResponses - .map((response) => getScheduleFromHtml(response, session)), + lectureResponses.map( + (e) => getScheduleFromHtml(e.item1, session, e.item2), + ), ).then((schedules) => schedules.expand((schedule) => schedule).toList()); lectures.sort((l1, l2) => l1.compare(l2)); diff --git a/uni/lib/controller/parsers/parser_course_units.dart b/uni/lib/controller/parsers/parser_course_units.dart index 8131dc51b..c57764d2a 100644 --- a/uni/lib/controller/parsers/parser_course_units.dart +++ b/uni/lib/controller/parsers/parser_course_units.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course.dart'; @@ -6,8 +7,9 @@ import 'package:uni/utils/url_parser.dart'; List parseCourseUnitsAndCourseAverage( http.Response response, - Course course, -) { + Course course, { + List? currentCourseUnits, +}) { final document = parse(response.body); final table = document.getElementById('tabelapercurso'); if (table == null) { @@ -68,11 +70,16 @@ List parseCourseUnitsAndCourseAverage( continue; } + final matchingCurrentCourseUnit = currentCourseUnits + ?.firstWhereOrNull((element) => element.code == codeName); + final courseUnit = CourseUnit( schoolYear: '${firstSchoolYear + yearIncrement}/${firstSchoolYear + yearIncrement + 1}', occurrId: int.parse(occurId), - abbreviation: codeName, + code: codeName, + abbreviation: matchingCurrentCourseUnit?.abbreviation ?? + codeName, // FIXME: this is not the abbreviation status: status, grade: grade, ects: double.parse(ects), diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 2a98c4853..0a950c7d0 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -32,10 +32,15 @@ Future> parseSchedule(http.Response response) async { final secBegin = lecture['hora_inicio'] as int; final subject = lecture['ucurr_sigla'] as String; final typeClass = lecture['tipo'] as String; - // TODO(luisd): this was marked as a double on the develop branch but the - // tests' example api returns an integer. At the moment there are no - // classes so I can't test this. - final blocks = (lecture['aula_duracao'] as int) * 2; + + // Note: aula_duracao is an integer when the lecture is 1 hour long + // or 2 hours long and so on. When the lecture is 1.5 hours long, it + // returns a double, with the value 1.5. + final lectureDuration = lecture['aula_duracao']; + final blocks = lectureDuration is double + ? (lectureDuration * 2).toInt() + : (lectureDuration as int) * 2; + final room = (lecture['sala_sigla'] as String).replaceAll(RegExp(r'\+'), '\n'); final teacher = lecture['doc_sigla'] as String; diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index 88ac5ce8c..6843f749d 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -11,6 +11,7 @@ import 'package:uni/model/entities/time_utilities.dart'; Future> getOverlappedClasses( Session session, Document document, + String faculty, ) async { final lecturesList = []; @@ -21,7 +22,7 @@ Future> getOverlappedClasses( final subject = element.querySelector('acronym > a')?.text; final typeClass = element .querySelector('td[headers=t1]') - ?.nodes[2] + ?.nodes[1] .text ?.trim() .replaceAll(RegExp('[()]+'), ''); @@ -37,22 +38,33 @@ Future> getOverlappedClasses( final classNumber = element.querySelector('td[headers=t6] > a')?.text; try { + final startTimeList = startTime?.split(':') ?? []; + if (startTimeList.isEmpty) { + throw FormatException( + 'Overlapping class $subject has invalid startTime', + ); + } final fullStartTime = monday.add( Duration( days: day, - hours: int.parse(startTime!.substring(0, 2)), - minutes: int.parse(startTime.substring(3, 5)), + hours: int.parse(startTimeList[0]), + minutes: int.parse(startTimeList[1]), ), ); - final link = + final href = element.querySelector('td[headers=t6] > a')?.attributes['href']; - if (link == null) { + if (href == null) { throw Exception(); } - final response = await NetworkRouter.getWithCookies(link, {}, session); + final response = await NetworkRouter.getWithCookies( + '$faculty$href', + {}, + session, + ); - final classLectures = await getScheduleFromHtml(response, session); + final classLectures = + await getScheduleFromHtml(response, session, faculty); lecturesList.add( classLectures @@ -88,6 +100,7 @@ Future> getOverlappedClasses( Future> getScheduleFromHtml( http.Response response, Session session, + String faculty, ) async { final document = parse(response.body); var semana = [0, 0, 0, 0, 0, 0]; @@ -148,7 +161,9 @@ Future> getScheduleFromHtml( }); lecturesList - ..addAll(await getOverlappedClasses(session, document)) + ..addAll( + await getOverlappedClasses(session, document, faculty), + ) ..sort((a, b) => a.compare(b)); return lecturesList; diff --git a/uni/lib/model/entities/lecture.dart b/uni/lib/model/entities/lecture.dart index f91658b2e..b93aac32c 100644 --- a/uni/lib/model/entities/lecture.dart +++ b/uni/lib/model/entities/lecture.dart @@ -51,10 +51,11 @@ class Lecture { String classNumber, int occurrId, ) { + final startTimeList = startTimeString.split(':'); final startTime = day.add( Duration( - hours: int.parse(startTimeString.substring(0, 2)), - minutes: int.parse(startTimeString.substring(3, 5)), + hours: int.parse(startTimeList[0]), + minutes: int.parse(startTimeList[1]), ), ); final endTime = startTime.add(Duration(minutes: 30 * blocks)); diff --git a/uni/lib/model/providers/lazy/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart index 6df3626c1..b739dd8d0 100644 --- a/uni/lib/model/providers/lazy/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/departures_fetcher.dart'; import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class BusStopProvider extends StateProviderNotifier { BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); @@ -33,20 +31,14 @@ class BusStopProvider extends StateProviderNotifier { } Future fetchUserBusTrips() async { - try { - for (final stopCode in configuredBusStops.keys) { - final stopTrips = await DeparturesFetcher.getNextArrivalsStop( - stopCode, - configuredBusStops[stopCode]!, - ); - _configuredBusStops[stopCode]?.trips = stopTrips; - } - _timeStamp = DateTime.now(); - updateStatus(RequestStatus.successful); - } catch (e) { - Logger().e('Failed to get Bus Stop information'); - updateStatus(RequestStatus.failed); + for (final stopCode in configuredBusStops.keys) { + final stopTrips = await DeparturesFetcher.getNextArrivalsStop( + stopCode, + configuredBusStops[stopCode]!, + ); + _configuredBusStops[stopCode]?.trips = stopTrips; } + _timeStamp = DateTime.now(); } Future addUserBusStop(String stopCode, BusStopData stopData) async { @@ -59,7 +51,6 @@ class BusStopProvider extends StateProviderNotifier { _configuredBusStops[stopCode] = stopData; } - updateStatus(RequestStatus.busy); await fetchUserBusTrips(); final db = AppBusStopDatabase(); @@ -69,7 +60,6 @@ class BusStopProvider extends StateProviderNotifier { Future removeUserBusStop( String stopCode, ) async { - updateStatus(RequestStatus.busy); _configuredBusStops.remove(stopCode); notifyListeners(); diff --git a/uni/lib/model/providers/lazy/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart index 5be749683..3e532b718 100644 --- a/uni/lib/model/providers/lazy/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -7,7 +7,6 @@ import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class CalendarProvider extends StateProviderNotifier { CalendarProvider() @@ -23,16 +22,9 @@ class CalendarProvider extends StateProviderNotifier { } Future fetchCalendar(Session session) async { - try { - _calendar = await CalendarFetcherHtml().getCalendar(session); - - final db = CalendarDatabase(); - unawaited(db.saveCalendar(calendar)); - - updateStatus(RequestStatus.successful); - } catch (e) { - updateStatus(RequestStatus.failed); - } + _calendar = await CalendarFetcherHtml().getCalendar(session); + final db = CalendarDatabase(); + unawaited(db.saveCalendar(calendar)); } @override diff --git a/uni/lib/model/providers/lazy/course_units_info_provider.dart b/uni/lib/model/providers/lazy/course_units_info_provider.dart index 1f1e76a0b..2a9901870 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -1,6 +1,5 @@ import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; @@ -8,7 +7,6 @@ import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class CourseUnitsInfoProvider extends StateProviderNotifier { CourseUnitsInfoProvider() @@ -26,33 +24,16 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { CourseUnit courseUnit, Session session, ) async { - updateStatus(RequestStatus.busy); - try { - _courseUnitsSheets[courseUnit] = await CourseUnitsInfoFetcher() - .fetchCourseUnitSheet(session, courseUnit.occurrId); - } catch (e) { - updateStatus(RequestStatus.failed); - Logger().e('Failed to get course unit sheet for ${courseUnit.name}: $e'); - return; - } - updateStatus(RequestStatus.successful); + _courseUnitsSheets[courseUnit] = await CourseUnitsInfoFetcher() + .fetchCourseUnitSheet(session, courseUnit.occurrId); } Future fetchCourseUnitClasses( CourseUnit courseUnit, Session session, ) async { - updateStatus(RequestStatus.busy); - try { - _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() - .fetchCourseUnitClasses(session, courseUnit.occurrId); - } catch (e) { - updateStatus(RequestStatus.failed); - Logger() - .e('Failed to get course unit classes for ${courseUnit.name}: $e'); - return; - } - updateStatus(RequestStatus.successful); + _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() + .fetchCourseUnitClasses(session, courseUnit.occurrId); } @override diff --git a/uni/lib/model/providers/lazy/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart index 4327df8b3..5f93c548a 100644 --- a/uni/lib/model/providers/lazy/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -10,7 +10,6 @@ import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class ExamProvider extends StateProviderNotifier { ExamProvider() @@ -56,21 +55,16 @@ class ExamProvider extends StateProviderNotifier { List userUcs, { required bool persistentSession, }) async { - try { - final exams = await ExamFetcher(profile.courses, userUcs) - .extractExams(session, parserExams); + final exams = await ExamFetcher(profile.courses, userUcs) + .extractExams(session, parserExams); - exams.sort((exam1, exam2) => exam1.begin.compareTo(exam2.begin)); + exams.sort((exam1, exam2) => exam1.begin.compareTo(exam2.begin)); - if (persistentSession) { - await AppExamsDatabase().saveNewExams(exams); - } - - _exams = exams; - updateStatus(RequestStatus.successful); - } catch (e) { - updateStatus(RequestStatus.failed); + if (persistentSession) { + await AppExamsDatabase().saveNewExams(exams); } + + _exams = exams; } Future updateFilteredExams() async { @@ -80,7 +74,7 @@ class ExamProvider extends StateProviderNotifier { } Future setFilteredExams(Map newFilteredExams) async { - unawaited(AppSharedPreferences.saveFilteredExams(filteredExamsTypes)); + unawaited(AppSharedPreferences.saveFilteredExams(newFilteredExams)); _filteredExamsTypes = Map.from(newFilteredExams); notifyListeners(); } diff --git a/uni/lib/model/providers/lazy/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart index 2952f0e57..f43da6743 100644 --- a/uni/lib/model/providers/lazy/faculty_locations_provider.dart +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -5,7 +5,6 @@ import 'package:uni/model/entities/location_group.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class FacultyLocationsProvider extends StateProviderNotifier { FacultyLocationsProvider() @@ -17,13 +16,9 @@ class FacultyLocationsProvider extends StateProviderNotifier { @override Future loadFromStorage() async { - updateStatus(RequestStatus.busy); _locations = await LocationFetcherAsset().getLocations(); - updateStatus(RequestStatus.successful); } @override - Future loadFromRemote(Session session, Profile profile) async { - updateStatus(RequestStatus.successful); - } + Future loadFromRemote(Session session, Profile profile) async {} } diff --git a/uni/lib/model/providers/lazy/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart index 891495761..48d162e33 100644 --- a/uni/lib/model/providers/lazy/home_page_provider.dart +++ b/uni/lib/model/providers/lazy/home_page_provider.dart @@ -2,7 +2,6 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/utils/favorite_widget_type.dart'; class HomePageProvider extends StateProviderNotifier { @@ -20,9 +19,7 @@ class HomePageProvider extends StateProviderNotifier { } @override - Future loadFromRemote(Session session, Profile profile) async { - updateStatus(RequestStatus.successful); - } + Future loadFromRemote(Session session, Profile profile) async {} void setHomePageEditingMode({required bool editingMode}) { _isEditing = editingMode; diff --git a/uni/lib/model/providers/lazy/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart index fd5789ef7..93201bdbd 100644 --- a/uni/lib/model/providers/lazy/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -10,7 +10,6 @@ import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class LectureProvider extends StateProviderNotifier { LectureProvider() @@ -42,20 +41,15 @@ class LectureProvider extends StateProviderNotifier { required bool persistentSession, ScheduleFetcher? fetcher, }) async { - try { - final lectures = - await getLecturesFromFetcherOrElse(fetcher, session, profile); + final lectures = + await getLecturesFromFetcherOrElse(fetcher, session, profile); - if (persistentSession) { - final db = AppLecturesDatabase(); - await db.saveNewLectures(lectures); - } - - _lectures = lectures; - updateStatus(RequestStatus.successful); - } catch (e) { - updateStatus(RequestStatus.failed); + if (persistentSession) { + final db = AppLecturesDatabase(); + await db.saveNewLectures(lectures); } + + _lectures = lectures; } Future> getLecturesFromFetcherOrElse( diff --git a/uni/lib/model/providers/lazy/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart index ee1ab2292..c1fb18199 100644 --- a/uni/lib/model/providers/lazy/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -6,7 +6,6 @@ import 'package:uni/model/entities/library_occupation.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupationProvider() @@ -28,16 +27,10 @@ class LibraryOccupationProvider extends StateProviderNotifier { } Future fetchLibraryOccupation(Session session) async { - try { - _occupation = await LibraryOccupationFetcherSheets() - .getLibraryOccupationFromSheets(session); + _occupation = await LibraryOccupationFetcherSheets() + .getLibraryOccupationFromSheets(session); - final db = LibraryOccupationDatabase(); - unawaited(db.saveOccupation(_occupation!)); - - updateStatus(RequestStatus.successful); - } catch (e) { - updateStatus(RequestStatus.failed); - } + final db = LibraryOccupationDatabase(); + unawaited(db.saveOccupation(_occupation!)); } } diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart index d795e070f..619090669 100644 --- a/uni/lib/model/providers/lazy/reference_provider.dart +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -8,7 +8,6 @@ import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/reference.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class ReferenceProvider extends StateProviderNotifier { ReferenceProvider() @@ -31,18 +30,11 @@ class ReferenceProvider extends StateProviderNotifier { } Future fetchUserReferences(Session session) async { - try { - final response = - await ReferenceFetcher().getUserReferenceResponse(session); + final response = await ReferenceFetcher().getUserReferenceResponse(session); - _references = await parseReferences(response); + _references = await parseReferences(response); - updateStatus(RequestStatus.successful); - - final referencesDb = AppReferencesDatabase(); - unawaited(referencesDb.saveNewReferences(references)); - } catch (e) { - updateStatus(RequestStatus.failed); - } + final referencesDb = AppReferencesDatabase(); + unawaited(referencesDb.saveNewReferences(references)); } } diff --git a/uni/lib/model/providers/lazy/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart index cebefd30e..d0db39f40 100644 --- a/uni/lib/model/providers/lazy/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -8,7 +8,6 @@ import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class RestaurantProvider extends StateProviderNotifier { RestaurantProvider() @@ -37,18 +36,12 @@ class RestaurantProvider extends StateProviderNotifier { } Future fetchRestaurants(Session session) async { - try { - final restaurants = await RestaurantFetcher().getRestaurants(session); + final restaurants = await RestaurantFetcher().getRestaurants(session); - final db = RestaurantDatabase(); - unawaited(db.saveRestaurants(restaurants)); + final db = RestaurantDatabase(); + unawaited(db.saveRestaurants(restaurants)); - _restaurants = filterPastMeals(restaurants); - - updateStatus(RequestStatus.successful); - } catch (e) { - updateStatus(RequestStatus.failed); - } + _restaurants = filterPastMeals(restaurants); } Future toggleFavoriteRestaurant( diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index c6f05d31a..8c85284fc 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -16,7 +16,6 @@ import 'package:uni/controller/parsers/parser_print_balance.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/model/request_status.dart'; class ProfileProvider extends StateProviderNotifier { ProfileProvider() @@ -43,10 +42,6 @@ class ProfileProvider extends StateProviderNotifier { fetchUserPrintBalance(session), fetchCourseUnitsAndCourseAverages(session) ]); - - if (status != RequestStatus.failed) { - updateStatus(RequestStatus.successful); - } } Future loadProfile() async { @@ -66,93 +61,79 @@ class ProfileProvider extends StateProviderNotifier { } Future fetchUserFees(Session session) async { - try { - final response = await FeesFetcher().getUserFeesResponse(session); - - final feesBalance = parseFeesBalance(response); - final feesLimit = parseFeesNextLimit(response); + final response = await FeesFetcher().getUserFeesResponse(session); - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + final feesBalance = parseFeesBalance(response); + final feesLimit = parseFeesNextLimit(response); - if (userPersistentInfo != null) { - final profileDb = AppUserDataDatabase(); - await profileDb.saveUserFees(feesBalance, feesLimit); - } + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); - _profile - ..feesBalance = feesBalance - ..feesLimit = feesLimit; - } catch (e) { - updateStatus(RequestStatus.failed); + if (userPersistentInfo != null) { + final profileDb = AppUserDataDatabase(); + await profileDb.saveUserFees(feesBalance, feesLimit); } + + _profile + ..feesBalance = feesBalance + ..feesLimit = feesLimit; } Future fetchUserPrintBalance(Session session) async { - try { - final response = await PrintFetcher().getUserPrintsResponse(session); - final printBalance = await getPrintsBalance(response); - - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - if (userPersistentInfo != null) { - final profileDb = AppUserDataDatabase(); - await profileDb.saveUserPrintBalance(printBalance); - } - - _profile.printBalance = printBalance; - } catch (e) { - updateStatus(RequestStatus.failed); + final response = await PrintFetcher().getUserPrintsResponse(session); + final printBalance = await getPrintsBalance(response); + + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + if (userPersistentInfo != null) { + final profileDb = AppUserDataDatabase(); + await profileDb.saveUserPrintBalance(printBalance); } + + _profile.printBalance = printBalance; } Future fetchUserInfo(Session session) async { - try { - final profile = await ProfileFetcher.fetchProfile(session); - final currentCourseUnits = - await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); - - _profile = profile ?? Profile(); - _profile.courseUnits = currentCourseUnits; - - updateStatus(RequestStatus.successful); - - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - if (userPersistentInfo != null) { - // Course units are saved later, so we don't it here - final profileDb = AppUserDataDatabase(); - await profileDb.insertUserData(_profile); - } - } catch (e) { - updateStatus(RequestStatus.failed); + final profile = await ProfileFetcher.fetchProfile(session); + final currentCourseUnits = + await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); + + _profile = profile ?? Profile(); + _profile.courseUnits = currentCourseUnits; + + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + if (userPersistentInfo != null) { + // Course units are saved later, so we don't it here + final profileDb = AppUserDataDatabase(); + await profileDb.insertUserData(_profile); } } Future fetchCourseUnitsAndCourseAverages(Session session) async { - try { - final courses = profile.courses; - final allCourseUnits = await AllCourseUnitsFetcher() - .getAllCourseUnitsAndCourseAverages(profile.courses, session); - - if (allCourseUnits != null) { - _profile.courseUnits = allCourseUnits; - } else { - // Current course units should already have been fetched, - // so this is not a fatal error - } - - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - if (userPersistentInfo != null) { - final coursesDb = AppCoursesDatabase(); - await coursesDb.saveNewCourses(courses); - - final courseUnitsDatabase = AppCourseUnitsDatabase(); - await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); - } - } catch (e) { - updateStatus(RequestStatus.failed); + final courses = profile.courses; + final allCourseUnits = + await AllCourseUnitsFetcher().getAllCourseUnitsAndCourseAverages( + profile.courses, + session, + currentCourseUnits: profile.courseUnits, + ); + + if (allCourseUnits != null) { + _profile.courseUnits = allCourseUnits; + } else { + // Current course units should already have been fetched, + // so this is not a fatal error + } + + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + if (userPersistentInfo != null) { + final coursesDb = AppCoursesDatabase(); + await coursesDb.saveNewCourses(courses); + + final courseUnitsDatabase = AppCourseUnitsDatabase(); + await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); } } diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index cccdc536e..cfd4276ad 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -49,9 +49,7 @@ class SessionProvider extends StateProviderNotifier { } @override - Future loadFromRemote(Session session, Profile profile) async { - updateStatus(RequestStatus.successful); - } + Future loadFromRemote(Session session, Profile profile) async {} void restoreSession( String username, @@ -73,8 +71,6 @@ class SessionProvider extends StateProviderNotifier { List faculties, { required bool persistentSession, }) async { - updateStatus(RequestStatus.busy); - Session? session; try { session = await NetworkRouter.login( @@ -84,9 +80,8 @@ class SessionProvider extends StateProviderNotifier { persistentSession: persistentSession, ); } catch (e) { - updateStatus(RequestStatus.failed); throw InternetStatusException( - Provider.of(context).getLocale(), + Provider.of(context, listen: false).getLocale(), ); } @@ -94,13 +89,11 @@ class SessionProvider extends StateProviderNotifier { final responseHtml = await NetworkRouter.loginInSigarra(username, password, faculties); - updateStatus(RequestStatus.failed); - if (isPasswordExpired(responseHtml) && context.mounted) { throw ExpiredCredentialsException(); } else { throw WrongCredentialsException( - Provider.of(context).getLocale(), + Provider.of(context, listen: false).getLocale(), ); } } @@ -121,6 +114,5 @@ class SessionProvider extends StateProviderNotifier { ); await acceptTermsAndConditions(); - updateStatus(RequestStatus.successful); } } diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 41e80ed28..81543fd8b 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -2,6 +2,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:synchronized/synchronized.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/entities/profile.dart'; @@ -34,6 +35,14 @@ abstract class StateProviderNotifier extends ChangeNotifier { DateTime? get lastUpdateTime => _lastUpdateTime; + void markAsInitialized() { + _initializedFromStorage = true; + _initializedFromRemote = true; + _status = RequestStatus.successful; + _lastUpdateTime = DateTime.now(); + notifyListeners(); + } + void markAsNotInitialized() { _initializedFromStorage = false; _initializedFromRemote = false; @@ -41,6 +50,11 @@ abstract class StateProviderNotifier extends ChangeNotifier { _lastUpdateTime = null; } + void _updateStatus(RequestStatus status) { + _status = status; + notifyListeners(); + } + Future _loadFromStorage() async { Logger().d('Loading $runtimeType info from storage'); @@ -48,8 +62,15 @@ abstract class StateProviderNotifier extends ChangeNotifier { runtimeType.toString(), ); - await loadFromStorage(); - notifyListeners(); + try { + await loadFromStorage(); + notifyListeners(); + } catch (e, stackTrace) { + await Sentry.captureException(e, stackTrace: stackTrace); + Logger() + .e('Failed to load $runtimeType info from storage: $e\n$stackTrace'); + } + Logger().i('Loaded $runtimeType info from storage'); } @@ -68,7 +89,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { if (!shouldReload) { Logger().d('Last info for $runtimeType is within cache period ' '(last updated on $_lastUpdateTime); skipping remote load'); - updateStatus(RequestStatus.successful); + _updateStatus(RequestStatus.successful); return; } @@ -77,42 +98,36 @@ abstract class StateProviderNotifier extends ChangeNotifier { if (!hasConnectivity) { Logger().w('No internet connection; skipping $runtimeType remote load'); - updateStatus(RequestStatus.successful); + _updateStatus(RequestStatus.successful); return; } - updateStatus(RequestStatus.busy); + _updateStatus(RequestStatus.busy); - await loadFromRemote(session, profile); + try { + await loadFromRemote(session, profile); - if (_status == RequestStatus.successful) { Logger().i('Loaded $runtimeType info from remote'); _lastUpdateTime = DateTime.now(); - notifyListeners(); + _updateStatus(RequestStatus.successful); + await AppSharedPreferences.setLastDataClassUpdateTime( runtimeType.toString(), _lastUpdateTime!, ); - } else if (_status == RequestStatus.failed) { - Logger().e('Failed to load $runtimeType info from remote'); - } else { + } catch (e, stackTrace) { + await Sentry.captureException(e, stackTrace: stackTrace); Logger() - .w('$runtimeType remote load method did not update request status'); + .e('Failed to load $runtimeType info from remote: $e\n$stackTrace'); + _updateStatus(RequestStatus.failed); } } - void updateStatus(RequestStatus status) { - _status = status; - notifyListeners(); - } - Future forceRefresh(BuildContext context) async { await _lock.synchronized(() async { - final session = - Provider.of(context, listen: false).session; - final profile = - Provider.of(context, listen: false).profile; - + final session = context.read().session; + final profile = context.read().profile; + _updateStatus(RequestStatus.busy); await _loadFromRemote(session, profile, force: true); }); } @@ -133,10 +148,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { _initializedFromRemote = true; - final session = - Provider.of(context, listen: false).session; - final profile = - Provider.of(context, listen: false).profile; + final session = context.read().session; + final profile = context.read().profile; await _loadFromRemote(session, profile); }); @@ -160,10 +173,6 @@ abstract class StateProviderNotifier extends ChangeNotifier { /// Loads data from the remote server into the provider. /// This will run once when the provider is first initialized. - /// If the data is not available from the remote server - /// or the data is filled into the provider on demand, - /// this method should simply set the - /// request status to [RequestStatus.successful]; - /// otherwise, it should set the status accordingly. + /// This method must not catch data loading errors. Future loadFromRemote(Session session, Profile profile); } diff --git a/uni/lib/view/common_widgets/pages_layouts/general/general.dart b/uni/lib/view/common_widgets/pages_layouts/general/general.dart index ef5753178..116f830be 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/utils/drawer_items.dart'; @@ -13,6 +15,8 @@ import 'package:uni/view/profile/profile.dart'; /// Page with a hamburger menu and the user profile picture abstract class GeneralPageViewState extends State { final double borderMargin = 18; + bool _loadedOnce = false; + bool _loading = true; Future onRefresh(BuildContext context); @@ -20,8 +24,42 @@ abstract class GeneralPageViewState extends State { @override Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) => onLoad(context)); - return getScaffold(context, getBody(context)); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (_loadedOnce) { + return; + } + _loadedOnce = true; + setState(() { + _loading = true; + }); + + try { + await onLoad(context); + } catch (e, stackTrace) { + Logger().e('Failed to load page info: $e\n$stackTrace'); + await Sentry.captureException(e, stackTrace: stackTrace); + } + + setState(() { + _loading = false; + }); + }); + + return getScaffold( + context, + _loading + ? const Flex( + direction: Axis.vertical, + children: [ + Expanded( + child: Center( + child: CircularProgressIndicator(), + ), + ) + ], + ) + : getBody(context), + ); } Widget getBody(BuildContext context) { diff --git a/uni/lib/view/course_unit_info/course_unit_info.dart b/uni/lib/view/course_unit_info/course_unit_info.dart index ade00b29d..dd858d9dd 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -6,13 +6,12 @@ import 'package:uni/model/providers/lazy/course_units_info_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; -import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_classes.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_sheet.dart'; -import 'package:uni/view/lazy_consumer.dart'; class CourseUnitDetailPageView extends StatefulWidget { const CourseUnitDetailPageView(this.courseUnit, {super.key}); + final CourseUnit courseUnit; @override @@ -91,50 +90,36 @@ class CourseUnitDetailPageViewState } Widget _courseUnitSheetView(BuildContext context) { - return LazyConsumer( - builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: Center( - child: Text( - S.of(context).no_info, - textAlign: TextAlign.center, - ), - ), - status: courseUnitsInfoProvider.status, - builder: () => CourseUnitSheetView( - courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]!, - ), - hasContentPredicate: - courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != - null && - courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]! - .sections.isNotEmpty, - ); - }, - ); + final sheet = context + .read() + .courseUnitsSheets[widget.courseUnit]; + + if (sheet == null || sheet.sections.isEmpty) { + return Center( + child: Text( + S.of(context).no_info, + textAlign: TextAlign.center, + ), + ); + } + + return CourseUnitSheetView(sheet); } Widget _courseUnitClassesView(BuildContext context) { - return LazyConsumer( - builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: Center( - child: Text( - S.of(context).no_class, - textAlign: TextAlign.center, - ), - ), - status: courseUnitsInfoProvider.status, - builder: () => CourseUnitClassesView( - courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit]!, - ), - hasContentPredicate: - courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit] != - null && - courseUnitsInfoProvider - .courseUnitsClasses[widget.courseUnit]!.isNotEmpty, - ); - }, - ); + final classes = context + .read() + .courseUnitsClasses[widget.courseUnit]; + + if (classes == null || classes.isEmpty) { + return Center( + child: Text( + S.of(context).no_class, + textAlign: TextAlign.center, + ), + ); + } + + return CourseUnitClassesView(classes); } } diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 3aeb60cf4..2c24105f8 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -112,11 +112,13 @@ class ExamCard extends GenericCard { children: [ if (locale == AppLocale.pt) ...[ DateRectangle( - date: '${exam.weekDay}, ${exam.begin.day} de ${exam.month}', + date: '''${exam.weekDay(locale)}, ''' + '''${exam.begin.day} de ${exam.month(locale)}''', ) ] else ...[ DateRectangle( - date: '${exam.weekDay}, ${exam.begin.day} ${exam.month}', + date: '''${exam.weekDay(locale)}, ''' + '''${exam.begin.day} ${exam.month(locale)}''', ) ], RowContainer( @@ -133,6 +135,7 @@ class ExamCard extends GenericCard { /// Creates a row for the exams which will be displayed under the closest /// date exam with a separator between them. Widget createSecondaryRowFromExam(BuildContext context, Exam exam) { + final locale = Provider.of(context).getLocale(); return Container( margin: const EdgeInsets.only(top: 8), child: RowContainer( @@ -143,7 +146,7 @@ class ExamCard extends GenericCard { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${exam.begin.day} de ${exam.month}', + '${exam.begin.day} de ${exam.month(locale)}', style: Theme.of(context).textTheme.bodyLarge, ), ExamTitle( diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index edb0e3451..039db9c19 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -1,5 +1,6 @@ -import 'dart:collection'; +import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; @@ -37,7 +38,10 @@ class ScheduleCard extends GenericCard { return LazyConsumer( builder: (context, lectureProvider) => RequestDependentWidgetBuilder( status: lectureProvider.status, - builder: () => generateSchedule(lectureProvider.lectures, context), + builder: () => Column( + mainAxisSize: MainAxisSize.min, + children: getScheduleRows(context, lectureProvider.lectures), + ), hasContentPredicate: lectureProvider.lectures.isNotEmpty, onNullContent: Center( child: Text( @@ -51,53 +55,55 @@ class ScheduleCard extends GenericCard { ); } - Widget generateSchedule( - UnmodifiableListView lectures, - BuildContext context, - ) { - final lectureList = List.of(lectures); - return Column( - mainAxisSize: MainAxisSize.min, - children: getScheduleRows(context, lectureList), - ); - } - List getScheduleRows(BuildContext context, List lectures) { final rows = []; - final now = DateTime.now(); - var added = 0; // Lectures added to widget - var lastAddedLectureDate = DateTime.now(); // Day of last added lecture - - for (var i = 0; added < 2 && i < lectures.length; i++) { - if (now.compareTo(lectures[i].endTime) < 0) { - if (lastAddedLectureDate.weekday != lectures[i].startTime.weekday && - lastAddedLectureDate.compareTo(lectures[i].startTime) <= 0) { - rows.add( - DateRectangle( - date: - Provider.of(context).getWeekdaysWithLocale()[ - (lectures[i].startTime.weekday - 1) % 7], - ), - ); - } - - rows.add(createRowFromLecture(context, lectures[i])); - lastAddedLectureDate = lectures[i].startTime; - added++; + final lecturesByDay = lectures + .groupListsBy( + (lecture) => lecture.startTime.weekday, + ) + .entries + .toList() + .sortedBy((element) { + // Sort by day of the week, but next days come first + final dayDiff = element.key - DateTime.now().weekday; + return dayDiff >= 0 ? dayDiff - 7 : dayDiff; + }).toList(); + + for (final dayLectures + in lecturesByDay.sublist(0, min(2, lecturesByDay.length))) { + final day = dayLectures.key; + final lectures = dayLectures.value + .where( + (element) => + // Hide finished lectures from today + element.startTime.weekday != DateTime.now().weekday || + element.endTime.isAfter(DateTime.now()), + ) + .toList(); + + if (lectures.isEmpty) { + continue; } - } - if (rows.isEmpty) { - rows - ..add( - DateRectangle( - date: Provider.of(context) - .getWeekdaysWithLocale()[lectures[0].startTime.weekday % 7], - ), - ) - ..add(createRowFromLecture(context, lectures[0])); + rows.add( + DateRectangle( + date: Provider.of(context) + .getWeekdaysWithLocale()[(day - 1) % 7], + ), + ); + + for (final lecture in lectures) { + rows.add(createRowFromLecture(context, lecture)); + } + + rows.add(const Padding(padding: EdgeInsets.only(bottom: 10))); + + if (lectures.length >= 2) { + break; + } } + return rows; } diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 49b19d748..ccb2b0305 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -69,9 +69,7 @@ class LazyConsumer extends StatelessWidget { // Finally, complete provider initialization if (context.mounted) { - // This will fail if the session initialization failed. - // That is the expected behavior. - await sessionFuture!.then((_) async { + await sessionFuture?.then((_) async { await provider!.ensureInitializedFromRemote(context); }); } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 8a0131a3e..a390331d3 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -8,7 +8,6 @@ import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/login_exceptions.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; import 'package:uni/view/home/widgets/exit_app_dialog.dart'; @@ -40,16 +39,19 @@ class LoginPageViewState extends State { final GlobalKey _formKey = GlobalKey(); bool _keepSignedIn = true; bool _obscurePasswordInput = true; + bool _loggingIn = false; Future _login(BuildContext context) async { final stateProviders = StateProviders.fromContext(context); final sessionProvider = stateProviders.sessionProvider; - if (sessionProvider.status != RequestStatus.busy && - _formKey.currentState!.validate()) { + if (!_loggingIn && _formKey.currentState!.validate()) { final user = usernameController.text.trim(); final pass = passwordController.text.trim(); try { + setState(() { + _loggingIn = true; + }); await sessionProvider.postAuthentication( context, user, @@ -58,9 +60,18 @@ class LoginPageViewState extends State { persistentSession: _keepSignedIn, ); if (context.mounted) { - handleLogin(sessionProvider.status, context); + await Navigator.pushReplacementNamed( + context, + '/${DrawerItem.navPersonalArea.title}', + ); + setState(() { + _loggingIn = false; + }); } } catch (error) { + setState(() { + _loggingIn = false; + }); if (error is ExpiredCredentialsException) { updatePasswordDialog(); } else if (error is InternetStatusException) { @@ -250,7 +261,7 @@ class LoginPageViewState extends State { Widget createStatusWidget(BuildContext context) { return Consumer( builder: (context, sessionProvider, _) { - if (sessionProvider.status == RequestStatus.busy) { + if (_loggingIn) { return const SizedBox( height: 60, child: @@ -262,17 +273,6 @@ class LoginPageViewState extends State { ); } - void handleLogin(RequestStatus? status, BuildContext context) { - if (status == RequestStatus.successful) { - Navigator.pushReplacementNamed( - context, - '/${DrawerItem.navPersonalArea.title}', - ); - } else if (status == RequestStatus.failed) { - ToastMessage.error(context, S.of(context).failed_login); - } - } - void updatePasswordDialog() { showDialog( context: context, diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 084a9d856..0651fe625 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -43,7 +43,7 @@ class SchedulePageView extends StatefulWidget { }); final List lectures; - final RequestStatus? scheduleStatus; + final RequestStatus scheduleStatus; final int weekDay = DateTime.now().weekday; @@ -144,7 +144,7 @@ class SchedulePageViewState extends GeneralPageViewState List createSchedule( BuildContext context, List lectures, - RequestStatus? scheduleStatus, + RequestStatus scheduleStatus, ) { final tabBarViewContent = []; for (var i = 0; i < 5; i++) { @@ -194,13 +194,13 @@ class SchedulePageViewState extends GeneralPageViewState BuildContext context, int day, List lectures, - RequestStatus? scheduleStatus, + RequestStatus scheduleStatus, ) { final weekday = Provider.of(context).getWeekdaysWithLocale()[day]; final aggLectures = SchedulePageView.groupLecturesByDay(lectures); return RequestDependentWidgetBuilder( - status: scheduleStatus ?? RequestStatus.none, + status: scheduleStatus, builder: () => dayColumnBuilder(day, aggLectures[day], context), hasContentPredicate: aggLectures[day].isNotEmpty, onNullContent: Center( diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 78292fb41..596254755 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -7,7 +7,7 @@ publish_to: 'none' # We do not publish to pub.dev # To change it manually, override the value in app_version.txt. # The app version code is automatically also bumped by CI. # Do not change it manually. -version: 1.7.0+190 +version: 1.7.9+199 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index 03779534f..4e37ebd3f 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -87,6 +87,8 @@ void main() { persistentSession: false, ); + examProvider.markAsInitialized(); + await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); @@ -123,6 +125,8 @@ void main() { persistentSession: false, ); + examProvider.markAsInitialized(); + await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index 0deb663d2..2f040fc0b 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -75,6 +75,8 @@ void main() { persistentSession: false, ); + scheduleProvider.markAsInitialized(); + await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('schedule-page-tab-1'))); diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 5c696a338..d424a2a48 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -90,7 +90,6 @@ void main() { expect(provider.exams.isNotEmpty, true); expect(provider.exams, [sopeExam]); - expect(provider.status, RequestStatus.successful); }); test('When given two exams', () async { @@ -105,7 +104,6 @@ void main() { persistentSession: false, ); - expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); }); @@ -135,7 +133,6 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); }); @@ -143,14 +140,15 @@ When given three exams but one is to be parsed out, when(parserExams.parseExams(any, any)) .thenAnswer((_) async => throw Exception('RIP')); - await provider.fetchUserExams( - parserExams, - profile, - session, - userUcs, - persistentSession: false, + throwsA( + () async => provider.fetchUserExams( + parserExams, + profile, + session, + userUcs, + persistentSession: false, + ), ); - expect(provider.status, RequestStatus.failed); }); test('When Exam is today in one hour', () async { @@ -177,7 +175,6 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); }); @@ -205,7 +202,6 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.status, RequestStatus.successful); expect(provider.exams, []); }); @@ -233,7 +229,6 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); }); }); diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 77edb5e27..78a1bbb3b 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -71,21 +71,20 @@ void main() { ); expect(provider.lectures, [lecture1, lecture2]); - expect(provider.status, RequestStatus.successful); }); test('When an error occurs while trying to obtain the schedule', () async { when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => throw Exception('💥')); - await provider.fetchUserLectures( - session, - profile, - fetcher: fetcherMock, - persistentSession: false, + throwsA( + () async => provider.fetchUserLectures( + session, + profile, + fetcher: fetcherMock, + persistentSession: false, + ), ); - - expect(provider.status, RequestStatus.failed); }); }); }