From 89a15c7607d4b64b6755b17f79b3f3331807762f Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 19 Dec 2022 18:23:17 +0000 Subject: [PATCH 001/100] Connect course unit info frontend with the new provider --- .../all_course_units_fetcher.dart | 2 +- .../course_units_info_fetcher.dart | 32 +++ .../current_course_units_fetcher.dart | 2 +- .../controller/fetchers/courses_fetcher.dart | 2 +- uni/lib/controller/fetchers/exam_fetcher.dart | 2 +- .../app_course_units_database.dart | 2 +- .../parsers/parser_course_unit_info.dart | 16 ++ .../parsers/parser_course_units.dart | 2 +- uni/lib/main.dart | 4 + .../{ => course_units}/course_unit.dart | 0 .../course_units/course_unit_class.dart | 5 + .../course_units/course_unit_sheet.dart | 5 + .../providers/course_units_info_provider.dart | 47 ++++ uni/lib/model/providers/exam_provider.dart | 2 +- .../providers/profile_state_provider.dart | 10 +- uni/lib/model/providers/state_providers.dart | 6 + .../course_unit_info/course_unit_info.dart | 92 +++++-- .../widgets/course_unit_sheet.dart | 229 ++++++++++++++++++ uni/lib/view/course_units/course_units.dart | 2 +- .../widgets/course_unit_card.dart | 2 +- 20 files changed, 431 insertions(+), 33 deletions(-) rename uni/lib/controller/fetchers/{ => course_units_fetcher}/all_course_units_fetcher.dart (95%) create mode 100644 uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart rename uni/lib/controller/fetchers/{ => course_units_fetcher}/current_course_units_fetcher.dart (94%) create mode 100644 uni/lib/controller/parsers/parser_course_unit_info.dart rename uni/lib/model/entities/{ => course_units}/course_unit.dart (100%) create mode 100644 uni/lib/model/entities/course_units/course_unit_class.dart create mode 100644 uni/lib/model/entities/course_units/course_unit_sheet.dart create mode 100644 uni/lib/model/providers/course_units_info_provider.dart create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart diff --git a/uni/lib/controller/fetchers/all_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart similarity index 95% rename from uni/lib/controller/fetchers/all_course_units_fetcher.dart rename to uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart index baa454ebf..6026f5ef6 100644 --- a/uni/lib/controller/fetchers/all_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart @@ -2,7 +2,7 @@ import 'package:logger/logger.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_course_units.dart'; import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/session.dart'; class AllCourseUnitsFetcher { diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart new file mode 100644 index 000000000..60fc3220b --- /dev/null +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -0,0 +1,32 @@ +import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; +import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/controller/parsers/parser_course_unit_info.dart'; +import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/entities/session.dart'; + +class CourseUnitsInfoFetcher implements SessionDependantFetcher { + @override + List getEndpoints(Session session) { + // if course unit is not from the main faculty, Sigarra redirects + final url = + '${NetworkRouter.getBaseUrl(session.faculties[0])}ucurr_geral.ficha_uc_view'; + return [url]; + } + + Future fetchCourseUnitSheet( + Session session, int occurrId) async { + final url = getEndpoints(session)[0]; + final response = await NetworkRouter.getWithCookies( + url, {'pv_ocorrencia_id': occurrId.toString()}, session); + return parseCourseUnitSheet(response); + } + + Future> fetchCourseUnitClasses( + Session session, int occurrId) async { + final url = getEndpoints(session)[0]; + final response = await NetworkRouter.getWithCookies( + url, {'pv_ocorrencia_id': occurrId.toString()}, session); + return parseCourseUnitClasses(response); + } +} diff --git a/uni/lib/controller/fetchers/current_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart similarity index 94% rename from uni/lib/controller/fetchers/current_course_units_fetcher.dart rename to uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart index 95f23004d..3cbfb0532 100644 --- a/uni/lib/controller/fetchers/current_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/session.dart'; class CurrentCourseUnitsFetcher implements SessionDependantFetcher { diff --git a/uni/lib/controller/fetchers/courses_fetcher.dart b/uni/lib/controller/fetchers/courses_fetcher.dart index d56ed3347..a864d1059 100644 --- a/uni/lib/controller/fetchers/courses_fetcher.dart +++ b/uni/lib/controller/fetchers/courses_fetcher.dart @@ -1,7 +1,7 @@ import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/session.dart'; /// Returns the user's current list of [CourseUnit]. diff --git a/uni/lib/controller/fetchers/exam_fetcher.dart b/uni/lib/controller/fetchers/exam_fetcher.dart index 47d5b0741..462bb9934 100644 --- a/uni/lib/controller/fetchers/exam_fetcher.dart +++ b/uni/lib/controller/fetchers/exam_fetcher.dart @@ -2,7 +2,7 @@ import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_exams.dart'; import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/entities/session.dart'; diff --git a/uni/lib/controller/local_storage/app_course_units_database.dart b/uni/lib/controller/local_storage/app_course_units_database.dart index 0c992777c..29bb850dc 100644 --- a/uni/lib/controller/local_storage/app_course_units_database.dart +++ b/uni/lib/controller/local_storage/app_course_units_database.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; import 'package:uni/controller/local_storage/app_database.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; class AppCourseUnitsDatabase extends AppDatabase { static const String createScript = diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart new file mode 100644 index 000000000..f80891a9a --- /dev/null +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -0,0 +1,16 @@ +import 'package:http/http.dart' as http; +import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; + +Future parseCourseUnitSheet(http.Response response) async { + return CourseUnitSheet( + {'goals': 'Grelhar', 'program': 'A arte da grelha. Cenas.'}); +} + +Future> parseCourseUnitClasses( + http.Response response) async { + return [ + CourseUnitClass(["José", "Gomes"]), + CourseUnitClass(["Mendes", "Pereira"]), + ]; +} diff --git a/uni/lib/controller/parsers/parser_course_units.dart b/uni/lib/controller/parsers/parser_course_units.dart index 9612d6073..1414226d1 100644 --- a/uni/lib/controller/parsers/parser_course_units.dart +++ b/uni/lib/controller/parsers/parser_course_units.dart @@ -1,7 +1,7 @@ import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/utils/url_parser.dart'; List parseCourseUnitsAndCourseAverage( diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 3bb59362c..f404d4fed 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -8,6 +8,7 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/on_start_up.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/model/providers/calendar_provider.dart'; +import 'package:uni/model/providers/course_units_info_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; import 'package:uni/model/providers/favorite_cards_provider.dart'; @@ -48,6 +49,7 @@ Future main() async { BusStopProvider(), RestaurantProvider(), ProfileStateProvider(), + CourseUnitsInfoProvider(), SessionProvider(), CalendarProvider(), FacultyLocationsProvider(), @@ -104,6 +106,8 @@ class MyAppState extends State { create: (context) => stateProviders.restaurantProvider), ChangeNotifierProvider( create: (context) => stateProviders.profileStateProvider), + ChangeNotifierProvider( + create: (context) => stateProviders.courseUnitsInfoProvider), ChangeNotifierProvider( create: (context) => stateProviders.sessionProvider), ChangeNotifierProvider( diff --git a/uni/lib/model/entities/course_unit.dart b/uni/lib/model/entities/course_units/course_unit.dart similarity index 100% rename from uni/lib/model/entities/course_unit.dart rename to uni/lib/model/entities/course_units/course_unit.dart diff --git a/uni/lib/model/entities/course_units/course_unit_class.dart b/uni/lib/model/entities/course_units/course_unit_class.dart new file mode 100644 index 000000000..b0b67e9f5 --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_class.dart @@ -0,0 +1,5 @@ +class CourseUnitClass { + List students; + + CourseUnitClass(this.students); +} diff --git a/uni/lib/model/entities/course_units/course_unit_sheet.dart b/uni/lib/model/entities/course_units/course_unit_sheet.dart new file mode 100644 index 000000000..ecf6b7158 --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_sheet.dart @@ -0,0 +1,5 @@ +class CourseUnitSheet { + Map sections; + + CourseUnitSheet(this.sections); +} diff --git a/uni/lib/model/providers/course_units_info_provider.dart b/uni/lib/model/providers/course_units_info_provider.dart new file mode 100644 index 000000000..f4fb7414c --- /dev/null +++ b/uni/lib/model/providers/course_units_info_provider.dart @@ -0,0 +1,47 @@ +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'; +import 'package:uni/model/entities/course_units/course_unit_sheet.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 { + final Map _courseUnitsSheets = {}; + final Map> _courseUnitsClasses = {}; + + UnmodifiableMapView get courseUnitsSheets => + UnmodifiableMapView(_courseUnitsSheets); + UnmodifiableMapView> + get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); + + getCourseUnitSheet(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); + } + + getCourseUnitClasses(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); + } +} diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index 2f6d0b701..245f3bb87 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -8,7 +8,7 @@ import 'package:uni/controller/local_storage/app_exams_database.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/parsers/parser_exams.dart'; import 'package:uni/model/request_status.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; diff --git a/uni/lib/model/providers/profile_state_provider.dart b/uni/lib/model/providers/profile_state_provider.dart index 619b7a6de..9a64e621c 100644 --- a/uni/lib/model/providers/profile_state_provider.dart +++ b/uni/lib/model/providers/profile_state_provider.dart @@ -3,7 +3,8 @@ import 'dart:collection'; import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; -import 'package:uni/controller/fetchers/current_course_units_fetcher.dart'; +import 'package:uni/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart'; +import 'package:uni/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/fees_fetcher.dart'; import 'package:uni/controller/fetchers/print_fetcher.dart'; import 'package:uni/controller/fetchers/profile_fetcher.dart'; @@ -14,15 +15,12 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/app_user_database.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; - -// ignore: always_use_package_imports -import '../../controller/fetchers/all_course_units_fetcher.dart'; +import 'package:uni/model/request_status.dart'; class ProfileStateProvider extends StateProviderNotifier { List _currUcs = []; diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index 7dde9d8b8..5b8c9b26b 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/model/providers/calendar_provider.dart'; +import 'package:uni/model/providers/course_units_info_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; import 'package:uni/model/providers/favorite_cards_provider.dart'; @@ -19,6 +20,7 @@ class StateProviders { final BusStopProvider busStopProvider; final RestaurantProvider restaurantProvider; final ProfileStateProvider profileStateProvider; + final CourseUnitsInfoProvider courseUnitsInfoProvider; final SessionProvider sessionProvider; final CalendarProvider calendarProvider; final FacultyLocationsProvider facultyLocationsProvider; @@ -33,6 +35,7 @@ class StateProviders { this.busStopProvider, this.restaurantProvider, this.profileStateProvider, + this.courseUnitsInfoProvider, this.sessionProvider, this.calendarProvider, this.facultyLocationsProvider, @@ -51,6 +54,8 @@ class StateProviders { Provider.of(context, listen: false); final profileStateProvider = Provider.of(context, listen: false); + final courseUnitsInfoProvider = + Provider.of(context, listen: false); final sessionProvider = Provider.of(context, listen: false); final calendarProvider = @@ -72,6 +77,7 @@ class StateProviders { busStopProvider, restaurantProvider, profileStateProvider, + courseUnitsInfoProvider, sessionProvider, calendarProvider, facultyLocationsProvider, 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 b9a28c8cb..b09ac683c 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -1,7 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/entities/course_unit.dart'; -import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/providers/course_units_info_provider.dart'; +import 'package:uni/model/providers/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_sheet.dart'; class CourseUnitDetailPageView extends StatefulWidget { final CourseUnit courseUnit; @@ -16,24 +23,73 @@ class CourseUnitDetailPageView extends StatefulWidget { class CourseUnitDetailPageViewState extends SecondaryPageViewState { + @override + void initState() { + super.initState(); + + // TODO: Handle this loading in a page generic way (#659) + WidgetsBinding.instance.addPostFrameCallback((_) { + final courseUnitsProvider = + Provider.of(context, listen: false); + final session = context.read().session; + + final CourseUnitSheet? courseUnitSheet = + courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; + if (courseUnitSheet == null) { + courseUnitsProvider.getCourseUnitSheet(widget.courseUnit, session); + } + + final List? courseUnitClasses = + courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; + if (courseUnitClasses == null) { + courseUnitsProvider.getCourseUnitClasses(widget.courseUnit, session); + } + }); + } + @override Widget getBody(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - PageTitle( - center: false, - name: widget.courseUnit.name, - ), - Container( - padding: const EdgeInsets.all(20), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Ano letivo: ${widget.courseUnit.schoolYear}'), - const SizedBox( - height: 20, + return DefaultTabController( + length: 2, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + PageTitle( + center: false, + name: widget.courseUnit.name, + ), + const TabBar( + tabs: [Tab(text: "Ficha"), Tab(text: "Turmas")], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 30, left: 20, right: 20), + child: TabBarView( + children: [ + _courseUnitSheetView(context), + _courseUnitClassesView(context), + ], + ), ), - Text( - 'Resultado: ${widget.courseUnit.grade == null || widget.courseUnit.grade!.isEmpty ? 'N/A' : widget.courseUnit.grade}') - ])) - ]); + ) + ])); + } + + Widget _courseUnitSheetView(BuildContext context) { + return Consumer( + builder: (context, courseUnitsInfoProvider, _) { + return RequestDependentWidgetBuilder( + context: context, + status: courseUnitsInfoProvider.status, + contentGenerator: (content, context) => + CourseUnitSheetView(widget.courseUnit.name, content), + content: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit], + contentChecker: + courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != + null, + onNullContent: const Text("Não foi possível obter a ficha da UC")); + }); + } + + Widget _courseUnitClassesView(BuildContext context) { + return Text("Turmas"); } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart new file mode 100644 index 000000000..33c728db5 --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; + +class CourseUnitSheetView extends StatelessWidget { + final double padding = 12.0; + final CourseUnitSheet courseUnitSheet; + final String courseUnitName; + + const CourseUnitSheetView(this.courseUnitName, this.courseUnitSheet, + {super.key}); + + @override + Widget build(BuildContext context) { + Color iconColor = Theme.of(context).primaryColor; + return Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Column(children: [ + _courseObjectiveWidget(iconColor), + _courseProgramWidget(iconColor), + //_courseEvaluationWidget(iconColor), + //_courseTeachersWidget(iconColor) + ])) //ListView(children: sections)), + ); + } + + // Widget _courseTeachersWidget(Color iconColor) { + // return ExpansionTile( + // iconColor: iconColor, + // key: Key('$courseUnitName - Docencia'), + // title: _sectionTitle('Docência'), + // tilePadding: const EdgeInsets.only(right: 20), + // children: [ + // Column( + // key: Key('$courseUnitName - Docencia Tables'), + // children: getTeachers(courseUnitSheet.getTeachers())) + // ]); + // } + // + // List getTeachers(Map> teachers) { + // final List widgets = []; + // for (String type in teachers.keys) { + // widgets.add(_subSectionTitle(type)); + // widgets.add(Table( + // columnWidths: const {1: FractionColumnWidth(.2)}, + // defaultVerticalAlignment: TableCellVerticalAlignment.middle, + // children: getTeachersTable(teachers[type]))); + // widgets.add(const Padding( + // padding: EdgeInsets.fromLTRB(0, 16, 0, 16), + // )); + // } + // widgets.removeLast(); + // return widgets; + // } + + /*List getTeachersTable(List teachers) { + final List teachersTableLines = []; + for (CourseUnitTeacher teacher in teachers) { + teachersTableLines.add(TableRow(children: [ + Container( + margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, left: 5.0), + child: Text( + teacher.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + Container( + margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, right: 5.0), + child: Align( + alignment: Alignment.centerRight, + child: Text(teacher.hours, style: const TextStyle(fontSize: 14))), + ) + ])); + } + return [ + TableRow(children: [ + Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 5.0), + child: const Text( + 'Docente', + style: TextStyle(fontSize: 14), + ), + ), + Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, right: 5.0), + child: const Align( + alignment: Alignment.centerRight, + child: Text( + 'Horas', + style: TextStyle(fontSize: 14), + )), + ) + ]) + ] + + teachersTableLines; + }*/ + + Widget _courseObjectiveWidget(Color iconColor) { + return ExpansionTile( + iconColor: iconColor, + title: _sectionTitle('Objetivos'), + key: Key(courseUnitName + ' - Objetivos'), + tilePadding: const EdgeInsets.only(right: 20), + children: [ + Container( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + courseUnitSheet.sections['goals']!, + key: Key(courseUnitName + ' - Objetivos Text'), + style: const TextStyle(fontWeight: FontWeight.w400), + )), + ), + ]); + } + + Widget _courseProgramWidget(Color iconColor) { + return ExpansionTile( + iconColor: iconColor, + title: _sectionTitle('Programa'), + key: Key('$courseUnitName - Programa'), + tilePadding: const EdgeInsets.only(right: 20), + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + courseUnitSheet.sections['program']!, + key: Key('$courseUnitName - Programa Text'), + style: const TextStyle(fontWeight: FontWeight.w400), + )), + ]); + } + +/* Widget _courseEvaluationWidget(Color iconColor) { + return ExpansionTile( + iconColor: iconColor, + title: _sectionTitle('Avaliação'), + key: Key(courseUnitName + ' - Avaliacao'), + tilePadding: const EdgeInsets.only(right: 20), + children: [ + Column(children: [ + Table( + key: Key(courseUnitName + ' - Avaliacao Table'), + columnWidths: const {1: FractionColumnWidth(.3)}, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: getEvaluationTable()), + ]) + ]); + }*/ + + /*List getEvaluationTable() { + final List evaluationTableLines = []; + for (CourseUnitEvaluationComponent component + in courseUnitSheet.evaluationComponents) { + evaluationTableLines.add(TableRow(children: [ + Container( + margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, left: 5.0), + child: Text( + component.designation, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + Container( + margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, right: 5.0), + child: Align( + alignment: Alignment.centerRight, + child: + Text(component.weight, style: const TextStyle(fontSize: 14))), + ) + ])); + } + return [ + TableRow(children: [ + Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 5.0), + child: const Text( + 'Designação', + style: TextStyle(fontSize: 14), + ), + ), + Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, right: 5.0), + child: const Align( + alignment: Alignment.centerRight, + child: Text( + 'Peso (%)', + style: TextStyle(fontSize: 14), + )), + ) + ]) + ] + + evaluationTableLines; + }*/ + + Widget _sectionTitle(String title) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color.fromRGBO(50, 50, 50, 100), + fontSize: 16, + fontWeight: FontWeight.w500), + ), + )); + } + + Widget _subSectionTitle(String title) { + return Container( + padding: const EdgeInsets.fromLTRB(5, 0, 0, 0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color.fromRGBO(50, 50, 50, 100), + fontSize: 15, + fontWeight: FontWeight.w400), + ), + )); + } +} diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index d45b605ce..b1e6e9aed 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/request_status.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; diff --git a/uni/lib/view/course_units/widgets/course_unit_card.dart b/uni/lib/view/course_units/widgets/course_unit_card.dart index a3df1298d..f4bc8c7a6 100644 --- a/uni/lib/view/course_units/widgets/course_unit_card.dart +++ b/uni/lib/view/course_units/widgets/course_unit_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/entities/course_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/course_unit_info/course_unit_info.dart'; From 1fe9b3a1220bc94f98475d8289d4ecdfd502289e Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 19 Dec 2022 19:01:10 +0000 Subject: [PATCH 002/100] Refine request dependant widget and generic expansion card --- .../generic_expansion_card.dart | 34 +-- .../request_dependent_widget_builder.dart | 8 +- .../course_unit_info/course_unit_info.dart | 5 +- .../widgets/course_unit_sheet.dart | 204 ++---------------- .../widgets/course_unit_sheet_card.dart | 23 ++ 5 files changed, 64 insertions(+), 210 deletions(-) create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index 9d88e714c..5d17fe1d4 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -1,24 +1,22 @@ -import 'package:flutter/material.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; +import 'package:flutter/material.dart'; -/// Card with a expansible child -abstract class GenericExpansionCard extends StatefulWidget { - const GenericExpansionCard({Key? key}) : super(key: key); +/// Card with an expansible child +abstract class GenericExpansionCard extends StatelessWidget { + final bool smallTitle; + final EdgeInsetsGeometry? cardMargin; - @override - State createState() { - return GenericExpansionCardState(); - } + const GenericExpansionCard( + {Key? key, this.smallTitle = false, this.cardMargin}) + : super(key: key); String getTitle(); Widget buildCardContent(BuildContext context); -} -class GenericExpansionCardState extends State { @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.fromLTRB(20, 10, 20, 0), + margin: cardMargin ?? const EdgeInsets.fromLTRB(20, 10, 20, 0), child: ExpansionTileCard( expandedTextColor: Theme.of(context).primaryColor, heightFactorCurve: Curves.ease, @@ -26,16 +24,18 @@ class GenericExpansionCardState extends State { expandedColor: (Theme.of(context).brightness == Brightness.light) ? const Color.fromARGB(0xf, 0, 0, 0) : const Color.fromARGB(255, 43, 43, 43), - title: Text(widget.getTitle(), - style: Theme.of(context) - .textTheme - .headline5 - ?.apply(color: Theme.of(context).primaryColor)), + title: Text(getTitle(), + style: smallTitle + ? Theme.of(context).textTheme.headline6 + : Theme.of(context) + .textTheme + .headline5 + ?.apply(color: Theme.of(context).primaryColor)), elevation: 0, children: [ Container( padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), - child: widget.buildCardContent(context), + child: buildCardContent(context), ) ], )); diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index faf021526..d3eda0529 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -2,8 +2,8 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; /// Wraps content given its fetch data from the redux store, @@ -18,7 +18,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { required this.contentGenerator, required this.content, required this.contentChecker, - required this.onNullContent}) + this.onNullContent}) : super(key: key); final BuildContext context; @@ -26,7 +26,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { final Widget Function(dynamic, BuildContext) contentGenerator; final dynamic content; final bool contentChecker; - final Widget onNullContent; + final Widget? onNullContent; static final AppLastUserInfoUpdateDatabase lastUpdateDatabase = AppLastUserInfoUpdateDatabase(); @@ -39,7 +39,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { case RequestStatus.none: return contentChecker ? contentGenerator(content, context) - : onNullContent; + : onNullContent ?? Container(); case RequestStatus.busy: return contentChecker ? contentGenerator(content, 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 b09ac683c..b7e2d868a 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -61,7 +61,7 @@ class CourseUnitDetailPageViewState ), Expanded( child: Padding( - padding: const EdgeInsets.only(top: 30, left: 20, right: 20), + padding: const EdgeInsets.only(top: 20), child: TabBarView( children: [ _courseUnitSheetView(context), @@ -84,8 +84,7 @@ class CourseUnitDetailPageViewState content: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit], contentChecker: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != - null, - onNullContent: const Text("Não foi possível obter a ficha da UC")); + null); }); } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index 33c728db5..ad0f6bc10 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'course_unit_sheet_card.dart'; + class CourseUnitSheetView extends StatelessWidget { - final double padding = 12.0; final CourseUnitSheet courseUnitSheet; final String courseUnitName; @@ -11,189 +12,36 @@ class CourseUnitSheetView extends StatelessWidget { @override Widget build(BuildContext context) { - Color iconColor = Theme.of(context).primaryColor; return Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.only(left: 10, right: 10), child: Column(children: [ - _courseObjectiveWidget(iconColor), - _courseProgramWidget(iconColor), + _courseObjectiveWidget(), + _courseProgramWidget(), //_courseEvaluationWidget(iconColor), //_courseTeachersWidget(iconColor) ])) //ListView(children: sections)), ); } - // Widget _courseTeachersWidget(Color iconColor) { - // return ExpansionTile( - // iconColor: iconColor, - // key: Key('$courseUnitName - Docencia'), - // title: _sectionTitle('Docência'), - // tilePadding: const EdgeInsets.only(right: 20), - // children: [ - // Column( - // key: Key('$courseUnitName - Docencia Tables'), - // children: getTeachers(courseUnitSheet.getTeachers())) - // ]); - // } - // - // List getTeachers(Map> teachers) { - // final List widgets = []; - // for (String type in teachers.keys) { - // widgets.add(_subSectionTitle(type)); - // widgets.add(Table( - // columnWidths: const {1: FractionColumnWidth(.2)}, - // defaultVerticalAlignment: TableCellVerticalAlignment.middle, - // children: getTeachersTable(teachers[type]))); - // widgets.add(const Padding( - // padding: EdgeInsets.fromLTRB(0, 16, 0, 16), - // )); - // } - // widgets.removeLast(); - // return widgets; - // } - - /*List getTeachersTable(List teachers) { - final List teachersTableLines = []; - for (CourseUnitTeacher teacher in teachers) { - teachersTableLines.add(TableRow(children: [ - Container( - margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, left: 5.0), - child: Text( - teacher.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - ), - Container( - margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, right: 5.0), - child: Align( - alignment: Alignment.centerRight, - child: Text(teacher.hours, style: const TextStyle(fontSize: 14))), - ) - ])); - } - return [ - TableRow(children: [ - Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 5.0), - child: const Text( - 'Docente', - style: TextStyle(fontSize: 14), - ), - ), - Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, right: 5.0), - child: const Align( - alignment: Alignment.centerRight, - child: Text( - 'Horas', - style: TextStyle(fontSize: 14), - )), - ) - ]) - ] + - teachersTableLines; - }*/ - - Widget _courseObjectiveWidget(Color iconColor) { - return ExpansionTile( - iconColor: iconColor, - title: _sectionTitle('Objetivos'), - key: Key(courseUnitName + ' - Objetivos'), - tilePadding: const EdgeInsets.only(right: 20), - children: [ - Container( - child: Align( - alignment: Alignment.centerLeft, - child: Text( - courseUnitSheet.sections['goals']!, - key: Key(courseUnitName + ' - Objetivos Text'), - style: const TextStyle(fontWeight: FontWeight.w400), - )), - ), - ]); + Widget _courseObjectiveWidget() { + return CourseUnitSheetCard( + 'Objetivos', Text(courseUnitSheet.sections['goals']!)); } - Widget _courseProgramWidget(Color iconColor) { - return ExpansionTile( - iconColor: iconColor, - title: _sectionTitle('Programa'), - key: Key('$courseUnitName - Programa'), - tilePadding: const EdgeInsets.only(right: 20), - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - courseUnitSheet.sections['program']!, - key: Key('$courseUnitName - Programa Text'), - style: const TextStyle(fontWeight: FontWeight.w400), - )), - ]); - } - -/* Widget _courseEvaluationWidget(Color iconColor) { - return ExpansionTile( - iconColor: iconColor, - title: _sectionTitle('Avaliação'), - key: Key(courseUnitName + ' - Avaliacao'), - tilePadding: const EdgeInsets.only(right: 20), - children: [ - Column(children: [ - Table( - key: Key(courseUnitName + ' - Avaliacao Table'), - columnWidths: const {1: FractionColumnWidth(.3)}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: getEvaluationTable()), - ]) - ]); - }*/ - - /*List getEvaluationTable() { - final List evaluationTableLines = []; - for (CourseUnitEvaluationComponent component - in courseUnitSheet.evaluationComponents) { - evaluationTableLines.add(TableRow(children: [ - Container( - margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, left: 5.0), + Widget _courseProgramWidget() { + return CourseUnitSheetCard( + 'Programa', + Align( + alignment: Alignment.centerLeft, child: Text( - component.designation, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - ), - Container( - margin: const EdgeInsets.only(top: 5.0, bottom: 8.0, right: 5.0), - child: Align( - alignment: Alignment.centerRight, - child: - Text(component.weight, style: const TextStyle(fontSize: 14))), - ) - ])); - } - return [ - TableRow(children: [ - Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 5.0), - child: const Text( - 'Designação', - style: TextStyle(fontSize: 14), - ), - ), - Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 8.0, right: 5.0), - child: const Align( - alignment: Alignment.centerRight, - child: Text( - 'Peso (%)', - style: TextStyle(fontSize: 14), - )), - ) - ]) - ] + - evaluationTableLines; - }*/ + courseUnitSheet.sections['program']!, + key: Key('$courseUnitName - Programa Text'), + style: const TextStyle(fontWeight: FontWeight.w400), + )), + ); + } Widget _sectionTitle(String title) { return Container( @@ -210,20 +58,4 @@ class CourseUnitSheetView extends StatelessWidget { ), )); } - - Widget _subSectionTitle(String title) { - return Container( - padding: const EdgeInsets.fromLTRB(5, 0, 0, 0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - title, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Color.fromRGBO(50, 50, 50, 100), - fontSize: 15, - fontWeight: FontWeight.w400), - ), - )); - } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart new file mode 100644 index 000000000..fc225e63f --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:uni/view/common_widgets/generic_expansion_card.dart'; + +class CourseUnitSheetCard extends GenericExpansionCard { + final String sectionTitle; + final Widget content; + + const CourseUnitSheetCard(this.sectionTitle, this.content, {key}) + : super( + key: key, + cardMargin: const EdgeInsets.only(bottom: 10), + smallTitle: true); + + @override + Widget buildCardContent(BuildContext context) { + return content; + } + + @override + String getTitle() { + return sectionTitle; + } +} From 8cf53b304e12dc51badc6dbc86b1641c95375233 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 19 Dec 2022 19:40:32 +0000 Subject: [PATCH 003/100] Parse html sections from sigarra sheet --- .../parsers/parser_course_unit_info.dart | 28 +++++++- .../widgets/course_unit_sheet.dart | 64 ++++++------------- uni/pubspec.yaml | 1 + 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index f80891a9a..3fb0473d8 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -1,10 +1,22 @@ +import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; Future parseCourseUnitSheet(http.Response response) async { - return CourseUnitSheet( - {'goals': 'Grelhar', 'program': 'A arte da grelha. Cenas.'}); + final document = parse(response.body); + final titles = document.querySelectorAll('#conteudoinner h3'); + final Map sections = {}; + + for (var title in titles) { + try { + sections[title.text] = _htmlAfterElement(response.body, title.outerHtml); + } catch (_) { + continue; + } + } + + return CourseUnitSheet(sections); } Future> parseCourseUnitClasses( @@ -14,3 +26,15 @@ Future> parseCourseUnitClasses( CourseUnitClass(["Mendes", "Pereira"]), ]; } + +/*String _parseGeneralDescription(Element titleElement, String body) { + final String htmlDescription = + _htmlAfterElement(body, titleElement.outerHtml); + final doc = parse(htmlDescription); + return parse(doc.body.text).documentElement.text; +}*/ + +String _htmlAfterElement(String body, String elementOuterHtml) { + final int index = body.indexOf(elementOuterHtml) + elementOuterHtml.length; + return body.substring(index, body.indexOf('

', index)); +} diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index ad0f6bc10..deca89947 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; - -import 'course_unit_sheet_card.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_sheet_card.dart'; class CourseUnitSheetView extends StatelessWidget { final CourseUnitSheet courseUnitSheet; @@ -12,50 +12,24 @@ class CourseUnitSheetView extends StatelessWidget { @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, + final List cards = []; + for (var section in courseUnitSheet.sections.entries) { + cards.add(CourseUnitSheetCard( + section.key, + HtmlWidget( + section.value, + renderMode: RenderMode.column, + onTapUrl: (url) { + print('tapped $url'); + return false; + }, + ))); + } + + return Expanded( child: Container( padding: const EdgeInsets.only(left: 10, right: 10), - child: Column(children: [ - _courseObjectiveWidget(), - _courseProgramWidget(), - //_courseEvaluationWidget(iconColor), - //_courseTeachersWidget(iconColor) - ])) //ListView(children: sections)), - ); - } - - Widget _courseObjectiveWidget() { - return CourseUnitSheetCard( - 'Objetivos', Text(courseUnitSheet.sections['goals']!)); - } - - Widget _courseProgramWidget() { - return CourseUnitSheetCard( - 'Programa', - Align( - alignment: Alignment.centerLeft, - child: Text( - courseUnitSheet.sections['program']!, - key: Key('$courseUnitName - Programa Text'), - style: const TextStyle(fontWeight: FontWeight.w400), - )), - ); - } - - Widget _sectionTitle(String title) { - return Container( - padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - title, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Color.fromRGBO(50, 50, 50, 100), - fontSize: 16, - fontWeight: FontWeight.w500), - ), - )); + child: ListView(children: cards) //ListView(children: sections)), + )); } } diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index d4dd59e30..00e3dc10d 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: latlong2: ^0.8.1 flutter_map_marker_popup: ^3.2.0 material_design_icons_flutter: ^5.0.6595 + flutter_widget_from_html: ^0.9.0 dev_dependencies: flutter_test: From d217f472d56a7cd7f63a00742803cf02629462e5 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 19 Dec 2022 22:57:15 +0000 Subject: [PATCH 004/100] Improve sheet card styling --- .../widgets/course_unit_sheet.dart | 96 +++++++++++++++++-- .../widgets/course_unit_sheet_card.dart | 2 +- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index deca89947..292cbaa0c 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -1,6 +1,12 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:html/dom.dart' as dom; +import 'package:provider/provider.dart'; +import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_sheet_card.dart'; class CourseUnitSheetView extends StatelessWidget { @@ -12,18 +18,12 @@ class CourseUnitSheetView extends StatelessWidget { @override Widget build(BuildContext context) { + final session = context.read().session; + final baseUrl = Uri.parse(NetworkRouter.getBaseUrl(session.faculties[0])); + final List cards = []; for (var section in courseUnitSheet.sections.entries) { - cards.add(CourseUnitSheetCard( - section.key, - HtmlWidget( - section.value, - renderMode: RenderMode.column, - onTapUrl: (url) { - print('tapped $url'); - return false; - }, - ))); + cards.add(_buildCard(section.key, section.value, baseUrl)); } return Expanded( @@ -32,4 +32,80 @@ class CourseUnitSheetView extends StatelessWidget { child: ListView(children: cards) //ListView(children: sections)), )); } + + CourseUnitSheetCard _buildCard( + String sectionTitle, String sectionContent, Uri baseUrl) { + return CourseUnitSheetCard( + sectionTitle, + HtmlWidget( + sectionContent, + renderMode: RenderMode.column, + baseUrl: baseUrl, + customWidgetBuilder: (element) { + if (element.className == "informa" || + element.className == "limpar") { + return Container(); + } + if (element.localName == 'table') { + try { + element = _preprocessTable(element); + final tBody = element.children + .firstWhere((element) => element.localName == 'tbody'); + final rows = tBody.children; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Table( + border: TableBorder.all(), + children: rows + .map((e) => TableRow( + children: e.children + .sublist(0, min(4, e.children.length)) + .map((e) => TableCell( + child: Padding( + padding: const EdgeInsets.all(8), + child: HtmlWidget( + e.outerHtml, + renderMode: RenderMode.column, + baseUrl: baseUrl, + )))) + .toList())) + .toList(), + )); + } catch (e) { + return null; + } + } + return null; + }, + )); + } + + dom.Element _preprocessTable(dom.Element tableElement) { + final processedTable = tableElement.clone(true); + final tBody = tableElement.children + .firstWhere((element) => element.localName == 'tbody'); + final rows = tBody.children; + + for (int i = 0; i < rows.length; i++) { + for (int j = 0; j < rows[i].children.length; j++) { + final cell = rows[i].children[j]; + if (cell.attributes['rowspan'] != null) { + final rowSpan = int.parse(cell.attributes['rowspan']!); + if (rowSpan <= 1) { + continue; + } + processedTable.children[0].children[i].children[j].innerHtml = ""; + for (int k = 1; k < rowSpan; k++) { + try { + processedTable.children[0].children[i + k].children + .insert(j, cell.clone(true)); + } catch (_) { + continue; + } + } + } + } + } + return processedTable; + } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart index fc225e63f..48ee0afa7 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart @@ -13,7 +13,7 @@ class CourseUnitSheetCard extends GenericExpansionCard { @override Widget buildCardContent(BuildContext context) { - return content; + return Container(padding: const EdgeInsets.only(top: 10), child: content); } @override From 544e3045112481715f556996ed6f907d3e82bd97 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 20 Dec 2022 01:30:55 +0000 Subject: [PATCH 005/100] Add classes fetcher and view --- uni/android/app/build.gradle | 4 +- .../course_units_info_fetcher.dart | 48 +++++++-- .../parsers/parser_course_unit_info.dart | 46 ++++++--- .../course_units/course_unit_class.dart | 15 ++- .../course_unit_info/course_unit_info.dart | 18 +++- .../widgets/course_unit_classes.dart | 98 +++++++++++++++++++ ...t_card.dart => course_unit_info_card.dart} | 4 +- .../widgets/course_unit_sheet.dart | 12 +-- 8 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_classes.dart rename uni/lib/view/course_unit_info/widgets/{course_unit_sheet_card.dart => course_unit_info_card.dart} (80%) diff --git a/uni/android/app/build.gradle b/uni/android/app/build.gradle index e1abc5e1c..5504cb24f 100644 --- a/uni/android/app/build.gradle +++ b/uni/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 // default is flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { @@ -52,7 +52,7 @@ android { applicationId "pt.up.fe.ni.uni" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 // default is flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart index 60fc3220b..248a11e7d 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -1,3 +1,5 @@ +import 'package:html/parser.dart'; +import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_course_unit_info.dart'; @@ -8,15 +10,14 @@ import 'package:uni/model/entities/session.dart'; class CourseUnitsInfoFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { - // if course unit is not from the main faculty, Sigarra redirects - final url = - '${NetworkRouter.getBaseUrl(session.faculties[0])}ucurr_geral.ficha_uc_view'; - return [url]; + final urls = NetworkRouter.getBaseUrlsFromSession(session).toList(); + return urls; } Future fetchCourseUnitSheet( Session session, int occurrId) async { - final url = getEndpoints(session)[0]; + // if course unit is not from the main faculty, Sigarra redirects + final url = '${getEndpoints(session)[0]}ucurr_geral.ficha_uc_view'; final response = await NetworkRouter.getWithCookies( url, {'pv_ocorrencia_id': occurrId.toString()}, session); return parseCourseUnitSheet(response); @@ -24,9 +25,38 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { Future> fetchCourseUnitClasses( Session session, int occurrId) async { - final url = getEndpoints(session)[0]; - final response = await NetworkRouter.getWithCookies( - url, {'pv_ocorrencia_id': occurrId.toString()}, session); - return parseCourseUnitClasses(response); + for (final endpoint in getEndpoints(session)) { + // Crawl classes from all courses that the course unit is offered in + final String courseChoiceUrl = + '${endpoint}it_listagem.lista_cursos_disciplina?pv_ocorrencia_id=$occurrId'; + final Response courseChoiceResponse = + await NetworkRouter.getWithCookies(courseChoiceUrl, {}, session); + final courseChoiceDocument = parse(courseChoiceResponse.body); + final urls = courseChoiceDocument + .querySelectorAll('a') + .where((element) => + element.attributes['href'] != null && + element.attributes['href']! + .contains('it_listagem.lista_turma_disciplina')) + .map((e) { + var url = e.attributes['href']; + if (url != null && !url.contains('sigarra.up.pt')) { + url = endpoint + url; + } + return url; + }).toList(); + + for (final url in urls) { + try { + final Response response = + await NetworkRouter.getWithCookies(url!, {}, session); + return parseCourseUnitClasses(response, endpoint); + } catch (_) { + continue; + } + } + } + + return []; } } diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 3fb0473d8..f049943af 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -19,20 +19,40 @@ Future parseCourseUnitSheet(http.Response response) async { return CourseUnitSheet(sections); } -Future> parseCourseUnitClasses( - http.Response response) async { - return [ - CourseUnitClass(["José", "Gomes"]), - CourseUnitClass(["Mendes", "Pereira"]), - ]; -} +List parseCourseUnitClasses( + http.Response response, String baseUrl) { + final List classes = []; + final document = parse(response.body); + final titles = document.querySelectorAll('#conteudoinner h3').sublist(1); + + for (final title in titles) { + final table = title.nextElementSibling; + final className = title.innerHtml.substring( + title.innerHtml.indexOf(' ') + 1, title.innerHtml.indexOf('&')); + + final studentRows = table?.querySelectorAll('tr').sublist(1); + final List students = []; + + if (studentRows != null) { + for (final row in studentRows) { + final columns = row.querySelectorAll('td.k.t'); + final studentName = columns[0].children[0].innerHtml; + final studentNumber = int.tryParse(columns[1].innerHtml.trim()) ?? 0; + final studentMail = columns[2].innerHtml; -/*String _parseGeneralDescription(Element titleElement, String body) { - final String htmlDescription = - _htmlAfterElement(body, titleElement.outerHtml); - final doc = parse(htmlDescription); - return parse(doc.body.text).documentElement.text; -}*/ + final studentPhoto = Uri.parse( + "${baseUrl}fotografias_service.foto?pct_cod=$studentNumber"); + final studentProfile = Uri.parse( + "${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber"); + students.add(CourseUnitStudent(studentName, studentNumber, studentMail, + studentPhoto, studentProfile)); + } + } + classes.add(CourseUnitClass(className, students)); + } + + return classes; +} String _htmlAfterElement(String body, String elementOuterHtml) { final int index = body.indexOf(elementOuterHtml) + elementOuterHtml.length; diff --git a/uni/lib/model/entities/course_units/course_unit_class.dart b/uni/lib/model/entities/course_units/course_unit_class.dart index b0b67e9f5..d1948287f 100644 --- a/uni/lib/model/entities/course_units/course_unit_class.dart +++ b/uni/lib/model/entities/course_units/course_unit_class.dart @@ -1,5 +1,16 @@ class CourseUnitClass { - List students; + String className; + List students; + CourseUnitClass(this.className, this.students); +} + +class CourseUnitStudent { + String name; + int number; + String mail; + Uri photo; + Uri profile; - CourseUnitClass(this.students); + CourseUnitStudent( + this.name, this.number, this.mail, this.photo, this.profile); } 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 b7e2d868a..f52cb9e24 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -8,6 +8,7 @@ import 'package:uni/model/providers/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'; class CourseUnitDetailPageView extends StatefulWidget { @@ -79,8 +80,7 @@ class CourseUnitDetailPageViewState return RequestDependentWidgetBuilder( context: context, status: courseUnitsInfoProvider.status, - contentGenerator: (content, context) => - CourseUnitSheetView(widget.courseUnit.name, content), + contentGenerator: (content, context) => CourseUnitSheetView(content), content: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit], contentChecker: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != @@ -89,6 +89,18 @@ class CourseUnitDetailPageViewState } Widget _courseUnitClassesView(BuildContext context) { - return Text("Turmas"); + return Consumer( + builder: (context, courseUnitsInfoProvider, _) { + return RequestDependentWidgetBuilder( + context: context, + status: courseUnitsInfoProvider.status, + contentGenerator: (content, context) => + CourseUnitsClassesView(content), + content: + courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit], + contentChecker: + courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit] != + null); + }); } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart new file mode 100644 index 000000000..58e8f1cc3 --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -0,0 +1,98 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CourseUnitsClassesView extends StatelessWidget { + final List classes; + const CourseUnitsClassesView(this.classes, {super.key}); + + @override + Widget build(BuildContext context) { + final Session session = context.read().session; + final List cards = []; + for (var courseUnitClass in classes) { + final bool isMyClass = courseUnitClass.students + .where((student) => + student.number == + (int.tryParse( + session.studentNumber.replaceAll(RegExp(r"\D"), "")) ?? + 0)) + .isNotEmpty; + cards.add(_buildCard( + isMyClass + ? '${courseUnitClass.className} *' + : courseUnitClass.className, + Column( + children: courseUnitClass.students + .map((student) => _buildStudentWidget(student, session)) + .toList(), + ))); + } + + return Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards) //ListView(children: sections)), + )); + } + + CourseUnitInfoCard _buildCard(String sectionTitle, Widget sectionContent) { + return CourseUnitInfoCard( + sectionTitle, + sectionContent, + ); + } + + Widget _buildStudentWidget(CourseUnitStudent student, Session session) { + final Future response = + NetworkRouter.getWithCookies(student.photo.toString(), {}, session); + return FutureBuilder( + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Container( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.fill, + image: snapshot.hasData + ? Image.memory(snapshot.data!.bodyBytes).image + : const AssetImage( + 'assets/images/profile_placeholder.png')))), + Expanded( + child: InkWell( + onTap: () => launchUrl(student.profile), + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(student.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyText1), + Opacity( + opacity: 0.8, + child: Text( + "up${student.number}", + )) + ])))) + ], + )); + }, + future: response, + ); + } +} diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart similarity index 80% rename from uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart rename to uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart index 48ee0afa7..3295b04cd 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet_card.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -class CourseUnitSheetCard extends GenericExpansionCard { +class CourseUnitInfoCard extends GenericExpansionCard { final String sectionTitle; final Widget content; - const CourseUnitSheetCard(this.sectionTitle, this.content, {key}) + const CourseUnitInfoCard(this.sectionTitle, this.content, {key}) : super( key: key, cardMargin: const EdgeInsets.only(bottom: 10), diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index 292cbaa0c..af9bad977 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -7,21 +7,19 @@ import 'package:provider/provider.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/providers/session_provider.dart'; -import 'package:uni/view/course_unit_info/widgets/course_unit_sheet_card.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; class CourseUnitSheetView extends StatelessWidget { final CourseUnitSheet courseUnitSheet; - final String courseUnitName; - const CourseUnitSheetView(this.courseUnitName, this.courseUnitSheet, - {super.key}); + const CourseUnitSheetView(this.courseUnitSheet, {super.key}); @override Widget build(BuildContext context) { final session = context.read().session; final baseUrl = Uri.parse(NetworkRouter.getBaseUrl(session.faculties[0])); - final List cards = []; + final List cards = []; for (var section in courseUnitSheet.sections.entries) { cards.add(_buildCard(section.key, section.value, baseUrl)); } @@ -33,9 +31,9 @@ class CourseUnitSheetView extends StatelessWidget { )); } - CourseUnitSheetCard _buildCard( + CourseUnitInfoCard _buildCard( String sectionTitle, String sectionContent, Uri baseUrl) { - return CourseUnitSheetCard( + return CourseUnitInfoCard( sectionTitle, HtmlWidget( sectionContent, From 1dcd475ae054792129087a6011d977a0312477d3 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 20 Dec 2022 02:05:38 +0000 Subject: [PATCH 006/100] Cache images and solve layout bug --- uni/lib/controller/load_info.dart | 17 ++++++++---- .../widgets/course_unit_classes.dart | 27 +++++++++---------- .../widgets/course_unit_sheet.dart | 8 +++--- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index 1b57c09e6..99e6eeab3 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -104,8 +104,7 @@ void loadLocalUserInfoToState(StateProviders stateProviders) async { stateProviders.examProvider.updateStateBasedOnLocalUserExams(); stateProviders.lectureProvider.updateStateBasedOnLocalUserLectures(); stateProviders.busStopProvider.updateStateBasedOnLocalUserBusStops(); - stateProviders.profileStateProvider - .updateStateBasedOnLocalProfile(); + stateProviders.profileStateProvider.updateStateBasedOnLocalProfile(); stateProviders.profileStateProvider.updateStateBasedOnLocalRefreshTimes(); stateProviders.lastUserInfoProvider.updateStateBasedOnLocalTime(); stateProviders.calendarProvider.updateStateBasedOnLocalCalendar(); @@ -121,11 +120,19 @@ Future handleRefresh(StateProviders stateProviders) async { Future loadProfilePicture(Session session, {forceRetrieval = false}) { final String studentNumber = session.studentNumber; - final String faculty = session.faculties[0]; + return loadUserProfilePicture(studentNumber, session, + forceRetrieval: forceRetrieval); +} + +Future loadUserProfilePicture(String studentNumber, Session session, + {forceRetrieval = false}) { + final String studentNumberDigits = + studentNumber.replaceAll(RegExp(r'\D'), ''); final String url = - 'https://sigarra.up.pt/$faculty/pt/fotografias_service.foto?pct_cod=$studentNumber'; + 'https://sigarra.up.pt/${session.faculties[0]}/pt/fotografias_service.foto?pct_cod=$studentNumberDigits'; final Map headers = {}; headers['cookie'] = session.cookies; - return loadFileFromStorageOrRetrieveNew('user_profile_picture', url, headers, + return loadFileFromStorageOrRetrieveNew( + '${studentNumber}_profile_picture', url, headers, forceRetrieval: forceRetrieval); } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart index 58e8f1cc3..f0f77df42 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -1,8 +1,8 @@ -import 'package:flutter/cupertino.dart'; +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:http/http.dart'; import 'package:provider/provider.dart'; -import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/session_provider.dart'; @@ -36,11 +36,9 @@ class CourseUnitsClassesView extends StatelessWidget { ))); } - return Expanded( - child: Container( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ListView(children: cards) //ListView(children: sections)), - )); + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards)); } CourseUnitInfoCard _buildCard(String sectionTitle, Widget sectionContent) { @@ -51,10 +49,10 @@ class CourseUnitsClassesView extends StatelessWidget { } Widget _buildStudentWidget(CourseUnitStudent student, Session session) { - final Future response = - NetworkRouter.getWithCookies(student.photo.toString(), {}, session); + final Future userImage = + loadUserProfilePicture("up${student.number}", session); return FutureBuilder( - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { return Container( padding: const EdgeInsets.only(bottom: 10), child: Row( @@ -66,8 +64,9 @@ class CourseUnitsClassesView extends StatelessWidget { shape: BoxShape.circle, image: DecorationImage( fit: BoxFit.fill, - image: snapshot.hasData - ? Image.memory(snapshot.data!.bodyBytes).image + image: snapshot.hasData && + snapshot.data!.lengthSync() > 0 + ? FileImage(snapshot.data!) as ImageProvider : const AssetImage( 'assets/images/profile_placeholder.png')))), Expanded( @@ -92,7 +91,7 @@ class CourseUnitsClassesView extends StatelessWidget { ], )); }, - future: response, + future: userImage, ); } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index af9bad977..5ae08c786 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -24,11 +24,9 @@ class CourseUnitSheetView extends StatelessWidget { cards.add(_buildCard(section.key, section.value, baseUrl)); } - return Expanded( - child: Container( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ListView(children: cards) //ListView(children: sections)), - )); + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards)); } CourseUnitInfoCard _buildCard( From f559a2f6e581c85e43b4cd7505bf49beb529b332 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 20 Dec 2022 14:53:49 +0000 Subject: [PATCH 007/100] Fix profile image distortion --- uni/lib/view/course_unit_info/widgets/course_unit_classes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart index f0f77df42..626a9ea95 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -63,7 +63,7 @@ class CourseUnitsClassesView extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - fit: BoxFit.fill, + fit: BoxFit.cover, image: snapshot.hasData && snapshot.data!.lengthSync() > 0 ? FileImage(snapshot.data!) as ImageProvider From 07d578c465bd4ef5e5e769047b2d8a1c539fbb1d Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 24 Feb 2023 01:05:33 +0000 Subject: [PATCH 008/100] random illustrations algorithm --- .../bus_stop_next_arrivals.dart | 21 +++++++++++++++++-- uni/lib/view/exams/exams.dart | 17 ++++++++++++--- uni/lib/view/schedule/schedule.dart | 14 ++++++++++--- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index dce5a19c4..498b5778c 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -5,6 +5,7 @@ import 'package:uni/model/app_state.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/entities/trip.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; @@ -93,14 +94,30 @@ class NextArrivalsState extends State /// Returns a list of widgets for a successfull request List requestSuccessful(context) { final List result = []; + final List images = [Image.asset('assets/images/bus.png'), Image.asset('assets/images/flat_bus.png')]; result.addAll(getHeader(context)); if (widget.busConfig.isNotEmpty) { result.addAll(getContent(context)); } else { - result.add(Text('Não existe nenhuma paragem configurada', - style: Theme.of(context).textTheme.headline6)); + result.add( + RandomImageWidget(images: images, width: 250, height: 250) + ); + result.add( + TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.transparent), + ), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage())), + child: const Text('Adiciona as tuas paragens', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e))), + ),); + result.add( + const Text('\nNão percas nenhum autocarro', style: TextStyle(fontSize: 15) + ),); } return result; diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 3a073fcff..868d5883b 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -3,6 +3,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:uni/model/app_state.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; @@ -63,13 +64,23 @@ class ExamsList extends StatelessWidget { /// Creates a column with all the user's exams. List createExamsColumn(context, List exams) { final List columns = []; + final List images = [Image.asset('assets/images/vacation.png'), Image.asset('assets/images/swim_guy.png')]; + columns.add(const ExamPageTitle()); if (exams.isEmpty) { columns.add(Center( - heightFactor: 2, - child: Text('Não possui exames marcados.', - style: Theme.of(context).textTheme.headline6), + heightFactor: 1.2, + child: Column( + children: [ + RandomImageWidget(images: images, width: 250, height: 250), + const Text('Não tens exames marcados', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e)), + ), + const Text('\nParece que estás de férias!', + style: TextStyle(fontSize: 15), + ), + ]) )); return columns; } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 73e706958..6803cf659 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -5,6 +5,7 @@ import 'package:uni/model/app_state.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; @@ -174,6 +175,8 @@ class SchedulePageViewState extends GeneralPageViewState Widget createScheduleByDay(BuildContext context, int day, List? lectures, RequestStatus? scheduleStatus) { final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); + final List images = [Image.asset('assets/images/school.png'), Image.asset('assets/images/teacher.png')]; + return RequestDependentWidgetBuilder( context: context, status: scheduleStatus ?? RequestStatus.none, @@ -181,8 +184,13 @@ class SchedulePageViewState extends GeneralPageViewState content: aggLectures[day], contentChecker: aggLectures[day].isNotEmpty, onNullContent: Center( - child: Text( - 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.')), - ); + child: Column( + children: [ + RandomImageWidget(images: images, width: 250, height: 250), + Text('Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', style: const TextStyle( + fontSize: 15,),) + ]) + )); } } + From 8eaf5888df9f4531460cc3f64aad6f3ef20f20e6 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 24 Feb 2023 09:08:23 +0000 Subject: [PATCH 009/100] Random empty state illustrations --- uni/assets/images/bus.png | Bin 0 -> 97144 bytes uni/assets/images/flat_bus.png | Bin 0 -> 29008 bytes uni/assets/images/school.png | Bin 0 -> 62080 bytes uni/assets/images/swim_guy.png | Bin 0 -> 51329 bytes uni/assets/images/teacher.png | Bin 0 -> 28519 bytes uni/assets/images/vacation.png | Bin 0 -> 44042 bytes .../Flutter/flutter_export_environment 2.sh | 13 ++++++ uni/lib/view/common_widgets/random_image.dart | 39 ++++++++++++++++++ 8 files changed, 52 insertions(+) create mode 100644 uni/assets/images/bus.png create mode 100644 uni/assets/images/flat_bus.png create mode 100644 uni/assets/images/school.png create mode 100644 uni/assets/images/swim_guy.png create mode 100644 uni/assets/images/teacher.png create mode 100644 uni/assets/images/vacation.png create mode 100755 uni/ios/Flutter/flutter_export_environment 2.sh create mode 100644 uni/lib/view/common_widgets/random_image.dart diff --git a/uni/assets/images/bus.png b/uni/assets/images/bus.png new file mode 100644 index 0000000000000000000000000000000000000000..1313c483feb2d6d6fc5101bd56d50ba982fee181 GIT binary patch literal 97144 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 Oy%c1Wr7J%e1^yp409@|? literal 0 HcmV?d00001 diff --git a/uni/assets/images/flat_bus.png b/uni/assets/images/flat_bus.png new file mode 100644 index 0000000000000000000000000000000000000000..dd96f4e085dbf62b82691539c10616f97fc6d47e GIT binary patch literal 29008 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

Xt<2DzU~y6htW9Sb0_D;au6a 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 literal 0 HcmV?d00001 diff --git a/uni/assets/images/swim_guy.png b/uni/assets/images/swim_guy.png new file mode 100644 index 0000000000000000000000000000000000000000..011b444bb0c167c927901f10b4eb4e5b3fbcc030 GIT binary patch literal 51329 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

^htaU*Zpi;V%+zFc)w_6!t}0P^hGOe#J@ z|2p=JV0~#+5oHQA2r86fm#t`2;{uI1L6K1=)gPkvoBrw@MVD%yV}|bR`0Qsp5sLWV zgYT@*&>ieHEG3)QJ^GX!@^KAs$q9uJ0mXa6ptxSmofY;%6FewDkOC^0)ugs?6LCoD%K+ct@yxXNkG>7o5AwM?#H&q9~ihw&iSJ z&gYCr%dhQj!W-TArS4_t^ke)Qd#`Zl6EjN)Tt=Xo{!&C|gHz27tGr0-1O`q$cj|_f zcnRr5Qsn+symJ7QlBP%yMiqPT^6)%jXn8`KhvM+Ua@4Wx@^fX@;@L&ll`U=u#=VF* z7<-z>Oc;&hXxGb}C853Fz4DY~iw#e{C_7)fCAWp42ul1lBzKgzS2KQ(*B$IU+JbGD_2N)9iAT>`XZB51`^qjI(w!c;g4gB++1$>jo6)2T7Q1_N8t=;JBdB`AkgiO1RLqcch-@?a~@PZABDuO`i6kZxK z*-(zrA-#5_$3Zs-+mR#QO?~clmrGw<^I9q$In0x{*_K$_zg|{du}Ut)ZF7TL_6am-oqmJj)+p<)z%|oWfDZV|b%Y??;A6@_Np?p83z^pKi{0&V=mkX5ykBhI2mK z(@?rQ{W!zXona_Jqv8fY&f3jJKp2xi=b#YVpk55RFj&gi7DXv>Ddh~6p?@0}$3W;h zs&v*+N|AkqC4&ux@KR$nQ(nW%S^RFZGUoIoZe&|=>B>M-IC1>AG!7mFCVq*W_onlu zegL~4r7#g_$Jg#2|LL#8OtPYB&ADI&HtL*~#~=88CLd&iBquYqpJ_J!jBebW&$hjB zbH>kSL&lGt2HniFtsAomWBW5^>LmK%RFjIGx|y}xVPPFtx)$Z&=Cz$c6#(&_1|Ql^ zjN?cR9+?i6p<@&|8X!iVpQDmH!h8{s)*94LBpLcVh}!$r1~cYA2fO6o_JNzEh9AdO zst5wB|Mhk`j-!KVp&93$C08z)hh?2*@}qBjU-mgJHp+*CVXDTT^V!(D>4sVO?MsY* zXI#!SozL`d+ff|PWd_dLDfZ*AIr5z@@|c7Ap?q(J`e!oY9R7SeL9($2afLQ<^PRC0 zJ%%Q6_&h}IXlRiYwL5SOyQ0@2{XiP3qZ}nYp1?-66%dODMH8k?RAwF&m_@|1@tK$9 z)!+WHh>-CN(hpY?kMFvOX!coi60>I%!qC#ZZkPAM(Olp7t(g4y&Z?` zX6%if^Vu1vaX0q%voYJcowl(zb`-@QgxU+mnLC+%+z4g55P2?{XLZf;xh|hLDj6I< zh(O*S3sJi%3mQAyf7rA(>;&_rH~WHR`MjumOb~eN2mdMixAU%!^w5;E7rGX93I?kG z^P7JwSG?^?d=sE=4+d-k7=}pQkeAuGJ9XpV#AUl1H`|V8;EH+R~R zk&~fw*3L606wk+qh9a-?OU*BEY6Ih|L?2&j*7JrhJ462pzL@|38}sHQLLQJ`?kNeC zkmgHB5Lz^-s!t!8Q{)F&qEQ0Pg+XB3E3eCw|L;EYm5x6RX+2z z&&s$d<9jet=Syfm)9go?blt+*ZpPh?)41EZ)7^eHX8Jc~>V}Njb|YiwPKy0qDn{b? z3y|fKwgjN;+Yv{s340?3VwSt!WQG_O#q_``xl_YhW8`dnQt$rxV9I|8cg2wl7{&i#LTnNX>a@*v)8i;6 z^7%95P1upYbom0Q4QPkIZ{P#=xccAjkbMaor1*ho>dM7_yk-3GQ@oemaD^;5dy)L$ zYyTnZUdXO~;a%+RHEFhqy=g9Yl{{a5&D71Dniier(22<7B9g0S~U6_m$PJeQ5x-j+Qr!SCC~ zkt#AZ#f!`D@;-$|x3torO-rD-{pgXSvL52+wdk;4e(ELJjH^#iV0Qj0z-EU=;`1ZV zz9Nr84e4Je7od~B2uGe~fyoDPxlT5faQ@f*?rC}MzgLHoScFpHa?#Wk3kuHB@X^EN z<6roMJc94T|NOf@mt$xzB*Rdt+r&0w?2Os8Bl>f?*

D{!Lh?e`9ZSJB;nu*xPo_ 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

H(SP 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 literal 0 HcmV?d00001 diff --git a/uni/ios/Flutter/flutter_export_environment 2.sh b/uni/ios/Flutter/flutter_export_environment 2.sh new file mode 100755 index 000000000..97dee9d29 --- /dev/null +++ b/uni/ios/Flutter/flutter_export_environment 2.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.5.4" +export "FLUTTER_BUILD_NUMBER=122" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/uni/lib/view/common_widgets/random_image.dart b/uni/lib/view/common_widgets/random_image.dart new file mode 100644 index 000000000..a9613a144 --- /dev/null +++ b/uni/lib/view/common_widgets/random_image.dart @@ -0,0 +1,39 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class RandomImageWidget extends StatefulWidget { + final List images; + final double width; + final double height; + + const RandomImageWidget({required this.images, required this.width, required this.height, Key? key}) : super(key: key); + + @override + State createState() => _RandomImageWidgetState(); +} + +class _RandomImageWidgetState extends State { + late final List> _imageProviders; + late final Random _random; + + @override + void initState() { + super.initState(); + _random = Random(); + _imageProviders = widget.images.map((image) => image.image).toList(); + } + + ImageProvider _getRandomImageProvider() { + final index = _random.nextInt(_imageProviders.length); + return _imageProviders[index]; + } + + @override + Widget build(BuildContext context) { + return Image( + image: _getRandomImageProvider(), + width: widget.width, + height: widget.height, + ); + } +} From 2727c2838336da5a0b2bdeeabed147e8858c4df2 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 2 Mar 2023 22:23:03 +0000 Subject: [PATCH 010/100] Image randomizer algorithm refactor --- uni/ios/Flutter/Generated 2.xcconfig | 14 ++++++++ uni/ios/Flutter/Generated 3.xcconfig | 14 ++++++++ uni/ios/Flutter/Generated 4.xcconfig | 14 ++++++++ .../Flutter/flutter_export_environment 3.sh | 13 ++++++++ .../Flutter/flutter_export_environment 4.sh | 13 ++++++++ .../Flutter/flutter_export_environment 5.sh | 13 ++++++++ .../bus_stop_next_arrivals.dart | 8 +++-- uni/lib/view/common_widgets/random_image.dart | 33 ++++++++----------- uni/lib/view/exams/exams.dart | 8 +++-- uni/lib/view/schedule/schedule.dart | 10 ++++-- 10 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 uni/ios/Flutter/Generated 2.xcconfig create mode 100644 uni/ios/Flutter/Generated 3.xcconfig create mode 100644 uni/ios/Flutter/Generated 4.xcconfig create mode 100755 uni/ios/Flutter/flutter_export_environment 3.sh create mode 100755 uni/ios/Flutter/flutter_export_environment 4.sh create mode 100755 uni/ios/Flutter/flutter_export_environment 5.sh diff --git a/uni/ios/Flutter/Generated 2.xcconfig b/uni/ios/Flutter/Generated 2.xcconfig new file mode 100644 index 000000000..09d4df8dc --- /dev/null +++ b/uni/ios/Flutter/Generated 2.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/goiana/Desktop/flutter +FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.5.4 +FLUTTER_BUILD_NUMBER=122 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/uni/ios/Flutter/Generated 3.xcconfig b/uni/ios/Flutter/Generated 3.xcconfig new file mode 100644 index 000000000..9f43eb396 --- /dev/null +++ b/uni/ios/Flutter/Generated 3.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/goiana/Desktop/flutter +FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.5.6 +FLUTTER_BUILD_NUMBER=124 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/uni/ios/Flutter/Generated 4.xcconfig b/uni/ios/Flutter/Generated 4.xcconfig new file mode 100644 index 000000000..9f43eb396 --- /dev/null +++ b/uni/ios/Flutter/Generated 4.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/goiana/Desktop/flutter +FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.5.6 +FLUTTER_BUILD_NUMBER=124 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/uni/ios/Flutter/flutter_export_environment 3.sh b/uni/ios/Flutter/flutter_export_environment 3.sh new file mode 100755 index 000000000..4768674eb --- /dev/null +++ b/uni/ios/Flutter/flutter_export_environment 3.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.5.6" +export "FLUTTER_BUILD_NUMBER=124" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/uni/ios/Flutter/flutter_export_environment 4.sh b/uni/ios/Flutter/flutter_export_environment 4.sh new file mode 100755 index 000000000..9b10c321b --- /dev/null +++ b/uni/ios/Flutter/flutter_export_environment 4.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.5.7" +export "FLUTTER_BUILD_NUMBER=125" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/uni/ios/Flutter/flutter_export_environment 5.sh b/uni/ios/Flutter/flutter_export_environment 5.sh new file mode 100755 index 000000000..4768674eb --- /dev/null +++ b/uni/ios/Flutter/flutter_export_environment 5.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.5.6" +export "FLUTTER_BUILD_NUMBER=124" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 498b5778c..09cfa5a91 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -94,7 +94,7 @@ class NextArrivalsState extends State /// Returns a list of widgets for a successfull request List requestSuccessful(context) { final List result = []; - final List images = [Image.asset('assets/images/bus.png'), Image.asset('assets/images/flat_bus.png')]; + final List images = ['assets/images/bus.png', 'assets/images/flat_bus.png']; result.addAll(getHeader(context)); @@ -102,7 +102,11 @@ class NextArrivalsState extends State result.addAll(getContent(context)); } else { result.add( - RandomImageWidget(images: images, width: 250, height: 250) + RotatingImage( + imagePaths: images, + width: 250, + height: 250, + ), ); result.add( TextButton( diff --git a/uni/lib/view/common_widgets/random_image.dart b/uni/lib/view/common_widgets/random_image.dart index a9613a144..a5e2ed438 100644 --- a/uni/lib/view/common_widgets/random_image.dart +++ b/uni/lib/view/common_widgets/random_image.dart @@ -1,39 +1,32 @@ -import 'dart:math'; +import 'dart:async'; import 'package:flutter/material.dart'; -class RandomImageWidget extends StatefulWidget { - final List images; +class RotatingImage extends StatefulWidget { + final List imagePaths; final double width; final double height; - const RandomImageWidget({required this.images, required this.width, required this.height, Key? key}) : super(key: key); + const RotatingImage({required this.imagePaths, required this.width, required this.height, Key? key}) : super(key: key); @override - State createState() => _RandomImageWidgetState(); + State createState() => _RotatingImageState(); } -class _RandomImageWidgetState extends State { - late final List> _imageProviders; - late final Random _random; +class _RotatingImageState extends State { + int _index = 0; @override void initState() { super.initState(); - _random = Random(); - _imageProviders = widget.images.map((image) => image.image).toList(); - } - - ImageProvider _getRandomImageProvider() { - final index = _random.nextInt(_imageProviders.length); - return _imageProviders[index]; + Timer.periodic(const Duration(minutes: 1), (timer) { + setState(() { + _index = (_index + 1) % widget.imagePaths.length; + }); + }); } @override Widget build(BuildContext context) { - return Image( - image: _getRandomImageProvider(), - width: widget.width, - height: widget.height, - ); + return Image.asset(widget.imagePaths[_index], height: widget.height, width: widget.width,); } } diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 868d5883b..e39c97103 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -64,7 +64,7 @@ class ExamsList extends StatelessWidget { /// Creates a column with all the user's exams. List createExamsColumn(context, List exams) { final List columns = []; - final List images = [Image.asset('assets/images/vacation.png'), Image.asset('assets/images/swim_guy.png')]; + final List images = ['assets/images/vacation.png', 'assets/images/swim_guy.png']; columns.add(const ExamPageTitle()); @@ -73,7 +73,11 @@ class ExamsList extends StatelessWidget { heightFactor: 1.2, child: Column( children: [ - RandomImageWidget(images: images, width: 250, height: 250), + RotatingImage( + imagePaths: images, + width: 250, + height: 250, + ), const Text('Não tens exames marcados', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e)), ), diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 6803cf659..2193da6f2 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -175,18 +175,22 @@ class SchedulePageViewState extends GeneralPageViewState Widget createScheduleByDay(BuildContext context, int day, List? lectures, RequestStatus? scheduleStatus) { final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); - final List images = [Image.asset('assets/images/school.png'), Image.asset('assets/images/teacher.png')]; + final List images = ['assets/images/school.png', 'assets/images/teacher.png']; return RequestDependentWidgetBuilder( context: context, status: scheduleStatus ?? RequestStatus.none, contentGenerator: dayColumnBuilder(day), content: aggLectures[day], - contentChecker: aggLectures[day].isNotEmpty, + contentChecker: aggLectures[day].isEmpty, onNullContent: Center( child: Column( children: [ - RandomImageWidget(images: images, width: 250, height: 250), + RotatingImage( + imagePaths: images, + width: 250, + height: 250, + ), Text('Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', style: const TextStyle( fontSize: 15,),) ]) From 74b24c494d1994ae76beef94977ac1d75e3ea29e Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 2 Mar 2023 22:42:39 +0000 Subject: [PATCH 011/100] Reverted minor commit error --- .../{Generated 2.xcconfig => Generated 5.xcconfig} | 0 .../{Generated 3.xcconfig => Generated 6.xcconfig} | 0 .../{Generated 4.xcconfig => Generated 7.xcconfig} | 0 uni/ios/Flutter/flutter_export_environment 5.sh | 13 ------------- ...ronment 2.sh => flutter_export_environment 7.sh} | 0 ...ronment 3.sh => flutter_export_environment 8.sh} | 0 ...ronment 4.sh => flutter_export_environment 9.sh} | 0 uni/lib/view/schedule/schedule.dart | 2 +- 8 files changed, 1 insertion(+), 14 deletions(-) rename uni/ios/Flutter/{Generated 2.xcconfig => Generated 5.xcconfig} (100%) rename uni/ios/Flutter/{Generated 3.xcconfig => Generated 6.xcconfig} (100%) rename uni/ios/Flutter/{Generated 4.xcconfig => Generated 7.xcconfig} (100%) delete mode 100755 uni/ios/Flutter/flutter_export_environment 5.sh rename uni/ios/Flutter/{flutter_export_environment 2.sh => flutter_export_environment 7.sh} (100%) rename uni/ios/Flutter/{flutter_export_environment 3.sh => flutter_export_environment 8.sh} (100%) rename uni/ios/Flutter/{flutter_export_environment 4.sh => flutter_export_environment 9.sh} (100%) diff --git a/uni/ios/Flutter/Generated 2.xcconfig b/uni/ios/Flutter/Generated 5.xcconfig similarity index 100% rename from uni/ios/Flutter/Generated 2.xcconfig rename to uni/ios/Flutter/Generated 5.xcconfig diff --git a/uni/ios/Flutter/Generated 3.xcconfig b/uni/ios/Flutter/Generated 6.xcconfig similarity index 100% rename from uni/ios/Flutter/Generated 3.xcconfig rename to uni/ios/Flutter/Generated 6.xcconfig diff --git a/uni/ios/Flutter/Generated 4.xcconfig b/uni/ios/Flutter/Generated 7.xcconfig similarity index 100% rename from uni/ios/Flutter/Generated 4.xcconfig rename to uni/ios/Flutter/Generated 7.xcconfig diff --git a/uni/ios/Flutter/flutter_export_environment 5.sh b/uni/ios/Flutter/flutter_export_environment 5.sh deleted file mode 100755 index 4768674eb..000000000 --- a/uni/ios/Flutter/flutter_export_environment 5.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.5.6" -export "FLUTTER_BUILD_NUMBER=124" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/uni/ios/Flutter/flutter_export_environment 2.sh b/uni/ios/Flutter/flutter_export_environment 7.sh similarity index 100% rename from uni/ios/Flutter/flutter_export_environment 2.sh rename to uni/ios/Flutter/flutter_export_environment 7.sh diff --git a/uni/ios/Flutter/flutter_export_environment 3.sh b/uni/ios/Flutter/flutter_export_environment 8.sh similarity index 100% rename from uni/ios/Flutter/flutter_export_environment 3.sh rename to uni/ios/Flutter/flutter_export_environment 8.sh diff --git a/uni/ios/Flutter/flutter_export_environment 4.sh b/uni/ios/Flutter/flutter_export_environment 9.sh similarity index 100% rename from uni/ios/Flutter/flutter_export_environment 4.sh rename to uni/ios/Flutter/flutter_export_environment 9.sh diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 2193da6f2..5dac314b4 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -182,7 +182,7 @@ class SchedulePageViewState extends GeneralPageViewState status: scheduleStatus ?? RequestStatus.none, contentGenerator: dayColumnBuilder(day), content: aggLectures[day], - contentChecker: aggLectures[day].isEmpty, + contentChecker: aggLectures[day].isNotEmpty, onNullContent: Center( child: Column( children: [ From 18029c31d0f7ef41184660f20f665803391635b2 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 17 Mar 2023 17:20:15 +0000 Subject: [PATCH 012/100] Import fixing --- uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index d43c8530f..2586bcb16 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -6,7 +6,7 @@ import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; -import 'package:uni/view/bus_stop_next_arrivals/widgets/x.dart'; +import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; import 'package:uni/view/common_widgets/page_title.dart'; From 2de9be5f124a9043a986586128b6f870aa0bced4 Mon Sep 17 00:00:00 2001 From: Ricardo Matos Date: Tue, 11 Apr 2023 23:35:12 +0100 Subject: [PATCH 013/100] Refactoring code --- .../view/schedule/widgets/schedule_slot.dart | 179 ++++++++++++------ 1 file changed, 121 insertions(+), 58 deletions(-) diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index 89894ba32..74c17c8c5 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -47,31 +47,67 @@ class ScheduleSlot extends StatelessWidget { )); } - Widget createScheduleSlotTime(context) { - return Column( - key: Key('schedule-slot-time-$begin-$end'), - children: [ - createScheduleTime(begin, context), - createScheduleTime(end, context) - ], - ); + List createScheduleSlotPrimInfo(context) { + final subjectTextField = TextFieldWidget( + text: subject, + style: Theme.of(context) + .textTheme + .headline5! + .apply(color: Theme.of(context).colorScheme.tertiary), + alignment: TextAlign.center); + final typeClassTextField = TextFieldWidget( + text: ' ($typeClass)', + style: Theme.of(context).textTheme.bodyText2, + alignment: TextAlign.center); + final roomTextField = TextFieldWidget( + text: rooms, + style: Theme.of(context).textTheme.bodyText2, + alignment: TextAlign.right); + return [ + ScheduleTimeWidget(begin: begin, end: end), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SubjectButtonWidget( + occurrId: occurrId, + ), + subjectTextField, + typeClassTextField, + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ScheduleTeacherClassInfoWidget( + classNumber: classNumber, teacher: teacher)), + ], + )), + roomTextField + ]; } +} - Widget createScheduleTime(String time, context) => createTextField( - time, Theme.of(context).textTheme.bodyText2, TextAlign.center); +class SubjectButtonWidget extends StatelessWidget { + final int occurrId; + + const SubjectButtonWidget({super.key, required this.occurrId}); String toUcLink(int occurrId) { - const String faculty = 'feup'; //should not be hardcoded + const String faculty = 'feup'; // should not be hardcoded return '${NetworkRouter.getBaseUrl(faculty)}' 'UCURR_GERAL.FICHA_UC_VIEW?pv_ocorrencia_id=$occurrId'; } - _launchURL() async { + Future _launchURL() async { final String url = toUcLink(occurrId); await launchUrl(Uri.parse(url)); } - Widget createSubjectButton(BuildContext context) { + @override + Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -89,55 +125,82 @@ class ScheduleSlot extends StatelessWidget { ], ); } +} - List createScheduleSlotPrimInfo(context) { - final subjectTextField = createTextField( - subject, - Theme.of(context) - .textTheme - .headline5! - .apply(color: Theme.of(context).colorScheme.tertiary), - TextAlign.center); - final typeClassTextField = createTextField(' ($typeClass)', - Theme.of(context).textTheme.bodyText2, TextAlign.center); - final roomTextField = createTextField( - rooms, Theme.of(context).textTheme.bodyText2, TextAlign.right); - return [ - createScheduleSlotTime(context), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - createSubjectButton(context), - subjectTextField, - typeClassTextField, - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: createScheduleSlotTeacherClassInfo(context)), - ], - )), - roomTextField - ]; +class ScheduleTeacherClassInfoWidget extends StatelessWidget { + final String? classNumber; + final String teacher; + + const ScheduleTeacherClassInfoWidget( + {super.key, required this.teacher, this.classNumber}); + + @override + Widget build(BuildContext context) { + return TextFieldWidget( + text: classNumber != null ? '$classNumber | $teacher' : teacher, + style: Theme.of(context).textTheme.bodyText2, + alignment: TextAlign.center, + ); + } +} + +class ScheduleTimeWidget extends StatelessWidget { + final String begin; + final String end; + + const ScheduleTimeWidget({super.key, required this.begin, required this.end}); + + @override + Widget build(BuildContext context) { + return Column( + key: Key('schedule-slot-time-$begin-$end'), + children: [ + ScheduleTimeTextField(time: begin, context: context), + ScheduleTimeTextField(time: end, context: context), + ], + ); } +} - Widget createScheduleSlotTeacherClassInfo(context) { - return createTextField( - classNumber != null ? '$classNumber | $teacher' : teacher, - Theme.of(context).textTheme.bodyText2, - TextAlign.center); +class ScheduleTimeTextField extends StatelessWidget { + final String time; + final BuildContext context; + + const ScheduleTimeTextField( + {super.key, required this.time, required this.context}); + + @override + Widget build(BuildContext context) { + // TODO... VAMOS AO EXTREMO DE CRIAR UM WIDGET PARA TUDO ? + return TextFieldWidget( + text: time, + style: Theme.of(context).textTheme.bodyText2, + alignment: TextAlign.center, + ); } +} - Widget createTextField(text, style, alignment) { - return Text(text, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - style: style, - textAlign: alignment); +class TextFieldWidget extends StatelessWidget { + final String text; + final TextStyle? style; + final TextAlign alignment; + + const TextFieldWidget({ + super.key, + required this.text, + required this.style, + required this.alignment, + }); + + @override + Widget build(BuildContext context) { + return Text( + text, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, + style: style, + textAlign: alignment, + ); } } From d2c7ef6a3edd400ed08dd5a5fd232ed95870580b Mon Sep 17 00:00:00 2001 From: DGoiana Date: Wed, 12 Apr 2023 14:14:54 +0100 Subject: [PATCH 014/100] Requested changes --- .../bus_stop_next_arrivals.dart | 23 +++++++++---------- uni/lib/view/exams/exams.dart | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 2586bcb16..33a145577 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -94,19 +94,18 @@ class NextArrivalsState extends State { ), ); result.add( - TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.transparent), - ), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage())), - child: const Text('Adiciona as tuas paragens', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e))), - ),); + const Text('Não percas nenhum autocarro', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Color.fromARGB(255, 0x75, 0x17, 0x1e))), + ); result.add( - const Text('\nNão percas nenhum autocarro', style: TextStyle(fontSize: 15) - ),); + Container( + padding: EdgeInsets.only(top: 15), + child: ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const BusStopSelectionPage())), + child: const Text('Adicionar'), + ) + )); } return result; diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 06d3895dd..189cccbc3 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -52,10 +52,10 @@ class ExamsPageViewState extends GeneralPageViewState { width: 250, height: 250, ), - const Text('Não tens exames marcados', + const Text('Parece que estás de férias!', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e)), ), - const Text('\nParece que estás de férias!', + const Text('\nNão tens exames marcados', style: TextStyle(fontSize: 15), ), ]) From 78f59cd4684cdbeab5c81dde78943dfd4eec6e40 Mon Sep 17 00:00:00 2001 From: Ricardo Matos Date: Wed, 26 Apr 2023 15:14:38 +0100 Subject: [PATCH 015/100] eliminating last helper function --- .../view/schedule/widgets/schedule_slot.dart | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index 74c17c8c5..25bc49097 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -31,22 +31,18 @@ class ScheduleSlot extends StatelessWidget { child: Container( padding: const EdgeInsets.only( top: 10.0, bottom: 10.0, left: 22.0, right: 22.0), - child: createScheduleSlotRow(context), + child: Container( + key: Key('schedule-slot-time-$begin-$end'), + margin: const EdgeInsets.only(top: 3.0, bottom: 3.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: createScheduleSlotPrimInfo(context), + )), )); } - Widget createScheduleSlotRow(context) { - return Container( - key: Key('schedule-slot-time-$begin-$end'), - margin: const EdgeInsets.only(top: 3.0, bottom: 3.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: createScheduleSlotPrimInfo(context), - )); - } - List createScheduleSlotPrimInfo(context) { final subjectTextField = TextFieldWidget( text: subject, @@ -92,7 +88,7 @@ class ScheduleSlot extends StatelessWidget { class SubjectButtonWidget extends StatelessWidget { final int occurrId; - +0 const SubjectButtonWidget({super.key, required this.occurrId}); String toUcLink(int occurrId) { @@ -171,7 +167,6 @@ class ScheduleTimeTextField extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO... VAMOS AO EXTREMO DE CRIAR UM WIDGET PARA TUDO ? return TextFieldWidget( text: time, style: Theme.of(context).textTheme.bodyText2, From 10a68868718359467f148426291c6b442b024113 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sun, 30 Apr 2023 19:22:19 +0100 Subject: [PATCH 016/100] Cleanup code --- .../all_course_units_fetcher.dart | 2 +- .../course_units_info_fetcher.dart | 19 +++--- .../current_course_units_fetcher.dart | 7 +- .../parsers/parser_course_unit_info.dart | 13 ++-- uni/lib/view/common_widgets/generic_card.dart | 6 +- .../widgets/course_unit_classes.dart | 65 ++----------------- .../widgets/course_unit_student_row.dart | 64 ++++++++++++++++++ uni/lib/view/course_units/course_units.dart | 4 +- .../widgets/course_unit_card.dart | 2 +- .../widgets/restaurant_page_card.dart | 2 +- 10 files changed, 96 insertions(+), 88 deletions(-) create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart 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 6026f5ef6..4cb53205e 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 @@ -9,7 +9,7 @@ class AllCourseUnitsFetcher { Future> getAllCourseUnitsAndCourseAverages( List courses, Session session) async { final List allCourseUnits = []; - for (var course in courses) { + for (Course course in courses) { try { final List courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart index 248a11e7d..d2c5dd147 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -10,46 +10,45 @@ import 'package:uni/model/entities/session.dart'; class CourseUnitsInfoFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { - final urls = NetworkRouter.getBaseUrlsFromSession(session).toList(); - return urls; + return NetworkRouter.getBaseUrlsFromSession(session).toList(); } Future fetchCourseUnitSheet( Session session, int occurrId) async { // if course unit is not from the main faculty, Sigarra redirects - final url = '${getEndpoints(session)[0]}ucurr_geral.ficha_uc_view'; - final response = await NetworkRouter.getWithCookies( + final String url = '${getEndpoints(session)[0]}ucurr_geral.ficha_uc_view'; + final Response response = await NetworkRouter.getWithCookies( url, {'pv_ocorrencia_id': occurrId.toString()}, session); return parseCourseUnitSheet(response); } Future> fetchCourseUnitClasses( Session session, int occurrId) async { - for (final endpoint in getEndpoints(session)) { + for (String endpoint in getEndpoints(session)) { // Crawl classes from all courses that the course unit is offered in final String courseChoiceUrl = '${endpoint}it_listagem.lista_cursos_disciplina?pv_ocorrencia_id=$occurrId'; final Response courseChoiceResponse = await NetworkRouter.getWithCookies(courseChoiceUrl, {}, session); final courseChoiceDocument = parse(courseChoiceResponse.body); - final urls = courseChoiceDocument + final List urls = courseChoiceDocument .querySelectorAll('a') .where((element) => element.attributes['href'] != null && element.attributes['href']! .contains('it_listagem.lista_turma_disciplina')) .map((e) { - var url = e.attributes['href']; - if (url != null && !url.contains('sigarra.up.pt')) { + String? url = e.attributes['href']!; + if (!url.contains('sigarra.up.pt')) { url = endpoint + url; } return url; }).toList(); - for (final url in urls) { + for (String url in urls) { try { final Response response = - await NetworkRouter.getWithCookies(url!, {}, session); + await NetworkRouter.getWithCookies(url, {}, session); return parseCourseUnitClasses(response, endpoint); } catch (_) { continue; diff --git a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart index 3cbfb0532..9b234d626 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; @@ -9,14 +10,14 @@ class CurrentCourseUnitsFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { // all faculties list user course units on all faculties - final url = + final String url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}mob_fest_geral.ucurr_inscricoes_corrente'; return [url]; } Future> getCurrentCourseUnits(Session session) async { - final url = getEndpoints(session)[0]; - final response = await NetworkRouter.getWithCookies( + final String url = getEndpoints(session)[0]; + final Response response = await NetworkRouter.getWithCookies( url, {'pv_codigo': session.studentNumber}, session); if (response.statusCode == 200) { final responseBody = json.decode(response.body); diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index f049943af..0715d15a6 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -27,7 +27,7 @@ List parseCourseUnitClasses( for (final title in titles) { final table = title.nextElementSibling; - final className = title.innerHtml.substring( + final String className = title.innerHtml.substring( title.innerHtml.indexOf(' ') + 1, title.innerHtml.indexOf('&')); final studentRows = table?.querySelectorAll('tr').sublist(1); @@ -36,13 +36,14 @@ List parseCourseUnitClasses( if (studentRows != null) { for (final row in studentRows) { final columns = row.querySelectorAll('td.k.t'); - final studentName = columns[0].children[0].innerHtml; - final studentNumber = int.tryParse(columns[1].innerHtml.trim()) ?? 0; - final studentMail = columns[2].innerHtml; + final String studentName = columns[0].children[0].innerHtml; + final int studentNumber = + int.tryParse(columns[1].innerHtml.trim()) ?? 0; + final String studentMail = columns[2].innerHtml; - final studentPhoto = Uri.parse( + final Uri studentPhoto = Uri.parse( "${baseUrl}fotografias_service.foto?pct_cod=$studentNumber"); - final studentProfile = Uri.parse( + final Uri studentProfile = Uri.parse( "${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber"); students.add(CourseUnitStudent(studentName, studentNumber, studentMail, studentPhoto, studentProfile)); diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index dc92d9d04..e3952d745 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -4,7 +4,7 @@ import 'package:uni/model/entities/time_utilities.dart'; /// App default card abstract class GenericCard extends StatefulWidget { final EdgeInsetsGeometry margin; - final bool smallTitle; + final bool hasSmallTitle; final bool editingMode; final Function()? onDelete; @@ -20,7 +20,7 @@ abstract class GenericCard extends StatefulWidget { required this.editingMode, required this.onDelete, this.margin = const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - this.smallTitle = false}) + this.hasSmallTitle = false}) : super(key: key); @override @@ -104,7 +104,7 @@ class GenericCardState extends State { padding: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.only(top: 15, bottom: 10), child: Text(widget.getTitle(), - style: (widget.smallTitle + style: (widget.hasSmallTitle ? Theme.of(context) .textTheme .titleLarge! diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart index 626a9ea95..d66da9e11 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -1,13 +1,10 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_student_row.dart'; class CourseUnitsClassesView extends StatelessWidget { final List classes; @@ -17,7 +14,7 @@ class CourseUnitsClassesView extends StatelessWidget { Widget build(BuildContext context) { final Session session = context.read().session; final List cards = []; - for (var courseUnitClass in classes) { + for (CourseUnitClass courseUnitClass in classes) { final bool isMyClass = courseUnitClass.students .where((student) => student.number == @@ -25,13 +22,13 @@ class CourseUnitsClassesView extends StatelessWidget { session.studentNumber.replaceAll(RegExp(r"\D"), "")) ?? 0)) .isNotEmpty; - cards.add(_buildCard( + cards.add(CourseUnitInfoCard( isMyClass ? '${courseUnitClass.className} *' : courseUnitClass.className, Column( children: courseUnitClass.students - .map((student) => _buildStudentWidget(student, session)) + .map((student) => CourseUnitStudentRow(student, session)) .toList(), ))); } @@ -40,58 +37,4 @@ class CourseUnitsClassesView extends StatelessWidget { padding: const EdgeInsets.only(left: 10, right: 10), child: ListView(children: cards)); } - - CourseUnitInfoCard _buildCard(String sectionTitle, Widget sectionContent) { - return CourseUnitInfoCard( - sectionTitle, - sectionContent, - ); - } - - Widget _buildStudentWidget(CourseUnitStudent student, Session session) { - final Future userImage = - loadUserProfilePicture("up${student.number}", session); - return FutureBuilder( - builder: (BuildContext context, AsyncSnapshot snapshot) { - return Container( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.cover, - image: snapshot.hasData && - snapshot.data!.lengthSync() > 0 - ? FileImage(snapshot.data!) as ImageProvider - : const AssetImage( - 'assets/images/profile_placeholder.png')))), - Expanded( - child: InkWell( - onTap: () => launchUrl(student.profile), - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(student.name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1), - Opacity( - opacity: 0.8, - child: Text( - "up${student.number}", - )) - ])))) - ], - )); - }, - future: userImage, - ); - } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart new file mode 100644 index 000000000..abefa6eb3 --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/session.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:uni/controller/load_info.dart'; + +class CourseUnitStudentRow extends StatelessWidget { + const CourseUnitStudentRow(this.student, this.session, {super.key}); + + final CourseUnitStudent student; + final Session session; + + @override + Widget build(BuildContext context) { + final Future userImage = + loadUserProfilePicture("up${student.number}", session); + return FutureBuilder( + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Container( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: snapshot.hasData && + snapshot.data!.lengthSync() > 0 + ? FileImage(snapshot.data!) as ImageProvider + : const AssetImage( + 'assets/images/profile_placeholder.png')))), + Expanded( + child: InkWell( + onTap: () => launchUrl(student.profile), + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(student.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyLarge), + Opacity( + opacity: 0.8, + child: Text( + "up${student.number}", + )) + ])))) + ], + )); + }, + future: userImage, + ); + } +} diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 8019d29ef..47dbadef8 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -40,13 +40,13 @@ class CourseUnitsPageViewState (value, element) => element.compareTo(value) > 0 ? element : value); } - final currentYear = int.tryParse( + final int? currentYear = int.tryParse( selectedSchoolYear?.substring(0, selectedSchoolYear?.indexOf('/')) ?? ''); if (selectedSemester == null && currentYear != null && availableSemesters.length == 3) { - final currentDate = DateTime.now(); + final DateTime currentDate = DateTime.now(); selectedSemester = currentDate.year <= currentYear || currentDate.month == 1 ? availableSemesters[0] diff --git a/uni/lib/view/course_units/widgets/course_unit_card.dart b/uni/lib/view/course_units/widgets/course_unit_card.dart index f4bc8c7a6..32ff99481 100644 --- a/uni/lib/view/course_units/widgets/course_unit_card.dart +++ b/uni/lib/view/course_units/widgets/course_unit_card.dart @@ -11,7 +11,7 @@ class CourseUnitCard extends GenericCard { : super.customStyle( key: key, margin: const EdgeInsets.only(top: 10), - smallTitle: true, + hasSmallTitle: true, onDelete: () => null, editingMode: false); diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index 062fe8a88..77e67a480 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -7,7 +7,7 @@ class RestaurantPageCard extends GenericCard { RestaurantPageCard(this.restaurantName, this.meals, {super.key}) : super.customStyle( - editingMode: false, onDelete: () => null, smallTitle: true); + editingMode: false, onDelete: () => null, hasSmallTitle: true); @override Widget buildCardContent(BuildContext context) { From 323a2f02f83cb2f26ed26021eb00556d16d2408f Mon Sep 17 00:00:00 2001 From: DGoiana Date: Mon, 26 Jun 2023 20:06:57 +0100 Subject: [PATCH 017/100] Removing random image algorithm --- .../bus_stop_next_arrivals.dart | 8 +---- uni/lib/view/common_widgets/random_image.dart | 32 ------------------- uni/lib/view/exams/exams.dart | 8 +---- uni/lib/view/schedule/schedule.dart | 8 +---- 4 files changed, 3 insertions(+), 53 deletions(-) delete mode 100644 uni/lib/view/common_widgets/random_image.dart diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 33a145577..54f4df0cc 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; -import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; @@ -79,7 +78,6 @@ class NextArrivalsState extends State { /// Returns a list of widgets for a successfull request List requestSuccessful(context) { final List result = []; - final List images = ['assets/images/bus.png', 'assets/images/flat_bus.png']; result.addAll(getHeader(context)); @@ -87,11 +85,7 @@ class NextArrivalsState extends State { result.addAll(getContent(context)); } else { result.add( - RotatingImage( - imagePaths: images, - width: 250, - height: 250, - ), + Image.asset('assets/images/bus.png', height: 300, width: 300,), ); result.add( const Text('Não percas nenhum autocarro', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Color.fromARGB(255, 0x75, 0x17, 0x1e))), diff --git a/uni/lib/view/common_widgets/random_image.dart b/uni/lib/view/common_widgets/random_image.dart deleted file mode 100644 index a5e2ed438..000000000 --- a/uni/lib/view/common_widgets/random_image.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; - -class RotatingImage extends StatefulWidget { - final List imagePaths; - final double width; - final double height; - - const RotatingImage({required this.imagePaths, required this.width, required this.height, Key? key}) : super(key: key); - - @override - State createState() => _RotatingImageState(); -} - -class _RotatingImageState extends State { - int _index = 0; - - @override - void initState() { - super.initState(); - Timer.periodic(const Duration(minutes: 1), (timer) { - setState(() { - _index = (_index + 1) % widget.imagePaths.length; - }); - }); - } - - @override - Widget build(BuildContext context) { - return Image.asset(widget.imagePaths[_index], height: widget.height, width: widget.width,); - } -} diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 189cccbc3..7d72b54bd 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; -import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; @@ -38,7 +37,6 @@ class ExamsPageViewState extends GeneralPageViewState { /// Creates a column with all the user's exams. List createExamsColumn(context, List exams) { final List columns = []; - final List images = ['assets/images/vacation.png', 'assets/images/swim_guy.png']; columns.add(const ExamPageTitle()); @@ -47,11 +45,7 @@ class ExamsPageViewState extends GeneralPageViewState { heightFactor: 1.2, child: Column( children: [ - RotatingImage( - imagePaths: images, - width: 250, - height: 250, - ), + Image.asset('assets/images/vacation.png', height: 300, width: 300,), const Text('Parece que estás de férias!', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e)), ), diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 8c0347239..8fe37a05b 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -6,7 +6,6 @@ import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; -import 'package:uni/view/common_widgets/random_image.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; @@ -171,7 +170,6 @@ class SchedulePageViewState extends GeneralPageViewState Widget createScheduleByDay(BuildContext context, int day, List? lectures, RequestStatus? scheduleStatus) { final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); - final List images = ['assets/images/school.png', 'assets/images/teacher.png']; return RequestDependentWidgetBuilder( context: context, @@ -182,11 +180,7 @@ class SchedulePageViewState extends GeneralPageViewState onNullContent: Center( child: Column( children: [ - RotatingImage( - imagePaths: images, - width: 250, - height: 250, - ), + Image.asset('assets/images/school.png', height: 300, width: 300,), Text('Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', style: const TextStyle( fontSize: 15,),) ]) From 902513ab1d946a1177a57d89d3f2f6ad25dde1e1 Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:07:59 +0100 Subject: [PATCH 018/100] Delete Generated 5.xcconfig --- uni/ios/Flutter/Generated 5.xcconfig | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 uni/ios/Flutter/Generated 5.xcconfig diff --git a/uni/ios/Flutter/Generated 5.xcconfig b/uni/ios/Flutter/Generated 5.xcconfig deleted file mode 100644 index 09d4df8dc..000000000 --- a/uni/ios/Flutter/Generated 5.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/goiana/Desktop/flutter -FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_TARGET=lib/main.dart -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.5.4 -FLUTTER_BUILD_NUMBER=122 -EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 -EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json From 81bc88ce90461c5a636b94b2122cf2433730b15c Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:08:11 +0100 Subject: [PATCH 019/100] Delete Generated 6.xcconfig --- uni/ios/Flutter/Generated 6.xcconfig | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 uni/ios/Flutter/Generated 6.xcconfig diff --git a/uni/ios/Flutter/Generated 6.xcconfig b/uni/ios/Flutter/Generated 6.xcconfig deleted file mode 100644 index 9f43eb396..000000000 --- a/uni/ios/Flutter/Generated 6.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/goiana/Desktop/flutter -FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_TARGET=lib/main.dart -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.5.6 -FLUTTER_BUILD_NUMBER=124 -EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 -EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json From 21313dd59dc6b55aa99dd83e0ccbdfa58f2f78c9 Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:08:19 +0100 Subject: [PATCH 020/100] Delete Generated 7.xcconfig --- uni/ios/Flutter/Generated 7.xcconfig | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 uni/ios/Flutter/Generated 7.xcconfig diff --git a/uni/ios/Flutter/Generated 7.xcconfig b/uni/ios/Flutter/Generated 7.xcconfig deleted file mode 100644 index 9f43eb396..000000000 --- a/uni/ios/Flutter/Generated 7.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/goiana/Desktop/flutter -FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_TARGET=lib/main.dart -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.5.6 -FLUTTER_BUILD_NUMBER=124 -EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 -EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json From c6e44908f58b11616d6dcc333b1dae0d0e8fb2d2 Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:08:31 +0100 Subject: [PATCH 021/100] Delete flutter_export_environment 7.sh --- uni/ios/Flutter/flutter_export_environment 7.sh | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100755 uni/ios/Flutter/flutter_export_environment 7.sh diff --git a/uni/ios/Flutter/flutter_export_environment 7.sh b/uni/ios/Flutter/flutter_export_environment 7.sh deleted file mode 100755 index 97dee9d29..000000000 --- a/uni/ios/Flutter/flutter_export_environment 7.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.5.4" -export "FLUTTER_BUILD_NUMBER=122" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" From 478f84d57513bb6b8a4201f44cdff13f8bf883d4 Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:08:42 +0100 Subject: [PATCH 022/100] Delete flutter_export_environment 8.sh --- uni/ios/Flutter/flutter_export_environment 8.sh | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100755 uni/ios/Flutter/flutter_export_environment 8.sh diff --git a/uni/ios/Flutter/flutter_export_environment 8.sh b/uni/ios/Flutter/flutter_export_environment 8.sh deleted file mode 100755 index 4768674eb..000000000 --- a/uni/ios/Flutter/flutter_export_environment 8.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.5.6" -export "FLUTTER_BUILD_NUMBER=124" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" From 550dc1fba55b540956a470d129cb75e4d35573db Mon Sep 17 00:00:00 2001 From: Diogo Martins <81827192+DGoiana@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:08:55 +0100 Subject: [PATCH 023/100] Delete flutter_export_environment 9.sh --- uni/ios/Flutter/flutter_export_environment 9.sh | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100755 uni/ios/Flutter/flutter_export_environment 9.sh diff --git a/uni/ios/Flutter/flutter_export_environment 9.sh b/uni/ios/Flutter/flutter_export_environment 9.sh deleted file mode 100755 index 9b10c321b..000000000 --- a/uni/ios/Flutter/flutter_export_environment 9.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/goiana/Desktop/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/goiana/Desktop/project-schrodinger/uni" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.5.7" -export "FLUTTER_BUILD_NUMBER=125" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" From 09b536ee6c5b11acb9e8952cbe20b3d8e29719c5 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Mon, 26 Jun 2023 20:27:53 +0100 Subject: [PATCH 024/100] Lint fixing and unused images removal --- .github/workflows/deploy.yaml | 2 +- .github/workflows/test_lint.yaml | 4 +- uni/android/app/src/main/res/raw/keep.xml | 2 - uni/app_version.txt | 2 +- uni/assets/images/flat_bus.png | Bin 29008 -> 0 bytes uni/assets/images/swim_guy.png | Bin 51329 -> 0 bytes uni/assets/images/teacher.png | Bin 28519 -> 0 bytes .../background_workers/notifications.dart | 2 +- .../notifications/tuition_notification.dart | 25 +--- .../local_storage/app_bus_stop_database.dart | 2 +- .../local_storage/app_courses_database.dart | 1 - .../local_storage/app_lectures_database.dart | 10 +- .../local_storage/app_shared_preferences.dart | 9 +- .../notification_timeout_storage.dart | 4 +- uni/lib/controller/logout.dart | 1 + .../controller/networking/network_router.dart | 10 +- .../controller/parsers/parser_calendar.dart | 10 +- uni/lib/controller/parsers/parser_exams.dart | 9 +- .../controller/parsers/parser_schedule.dart | 12 +- .../parsers/parser_schedule_html.dart | 23 +-- uni/lib/model/entities/bug_report.dart | 28 ++-- uni/lib/model/entities/calendar_event.dart | 5 +- uni/lib/model/entities/exam.dart | 10 +- uni/lib/model/entities/lecture.dart | 75 +++++++--- uni/lib/model/entities/time_utilities.dart | 15 +- uni/lib/utils/duration_string_formatter.dart | 46 ------ uni/lib/view/about/about.dart | 3 +- uni/lib/view/bug_report/widgets/form.dart | 34 ++--- .../view/bug_report/widgets/text_field.dart | 6 +- .../bus_stop_next_arrivals.dart | 4 +- .../widgets/bus_stop_row.dart | 4 +- .../widgets/estimated_arrival_timestamp.dart | 3 +- .../widgets/trip_row.dart | 6 +- .../widgets/bus_stop_search.dart | 4 +- uni/lib/view/calendar/calendar.dart | 4 +- .../view/common_widgets/date_rectangle.dart | 2 +- uni/lib/view/common_widgets/generic_card.dart | 10 +- .../generic_expansion_card.dart | 2 +- .../common_widgets/last_update_timestamp.dart | 2 +- uni/lib/view/common_widgets/page_title.dart | 4 +- .../pages_layouts/general/general.dart | 3 +- .../general/widgets/navigation_drawer.dart | 2 +- .../request_dependent_widget_builder.dart | 16 +-- .../view/common_widgets/toast_message.dart | 4 +- uni/lib/view/course_units/course_units.dart | 4 +- uni/lib/view/exams/widgets/day_title.dart | 2 +- .../view/exams/widgets/exam_filter_form.dart | 7 +- uni/lib/view/exams/widgets/exam_row.dart | 9 +- uni/lib/view/exams/widgets/exam_time.dart | 6 +- uni/lib/view/exams/widgets/exam_title.dart | 9 +- uni/lib/view/home/widgets/bus_stop_card.dart | 6 +- uni/lib/view/home/widgets/exam_card.dart | 6 +- .../view/home/widgets/exam_card_shimmer.dart | 131 ++++++++---------- .../view/home/widgets/exit_app_dialog.dart | 2 +- .../view/home/widgets/main_cards_list.dart | 6 +- .../view/home/widgets/restaurant_card.dart | 2 +- uni/lib/view/home/widgets/schedule_card.dart | 31 +++-- .../home/widgets/schedule_card_shimmer.dart | 124 +++++++---------- uni/lib/view/library/library.dart | 6 +- .../widgets/library_occupation_card.dart | 6 +- uni/lib/view/locations/locations.dart | 5 + .../view/locations/widgets/faculty_maps.dart | 4 +- .../widgets/floorless_marker_popup.dart | 2 +- uni/lib/view/locations/widgets/icons.dart | 12 +- uni/lib/view/locations/widgets/map.dart | 12 +- uni/lib/view/locations/widgets/marker.dart | 2 +- .../view/locations/widgets/marker_popup.dart | 46 +++--- uni/lib/view/login/login.dart | 5 +- .../widgets/faculties_selection_form.dart | 3 +- uni/lib/view/navigation_service.dart | 5 +- .../profile/widgets/account_info_card.dart | 6 +- .../profile/widgets/course_info_card.dart | 14 +- .../widgets/create_print_mb_dialog.dart | 6 +- .../view/profile/widgets/print_info_card.dart | 4 +- .../view/restaurant/restaurant_page_view.dart | 37 ++--- .../widgets/restaurant_page_card.dart | 6 +- .../restaurant/widgets/restaurant_slot.dart | 3 +- uni/lib/view/schedule/schedule.dart | 10 +- .../view/schedule/widgets/schedule_slot.dart | 19 ++- uni/lib/view/splash/splash.dart | 10 +- .../widgets/terms_and_condition_dialog.dart | 4 +- uni/lib/view/theme.dart | 76 ++++------ .../view/useful_info/widgets/link_button.dart | 2 +- .../useful_info/widgets/text_components.dart | 7 +- uni/pubspec.yaml | 16 +-- 85 files changed, 490 insertions(+), 603 deletions(-) delete mode 100644 uni/android/app/src/main/res/raw/keep.xml delete mode 100644 uni/assets/images/flat_bus.png delete mode 100644 uni/assets/images/swim_guy.png delete mode 100644 uni/assets/images/teacher.png delete mode 100644 uni/lib/utils/duration_string_formatter.dart diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6289971b7..7ebeb3272 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -40,7 +40,7 @@ jobs: env: PROPERTIES_PATH: "android/key.properties" JAVA_VERSION: "11.x" - FLUTTER_VERSION: "3.7.2" + FLUTTER_VERSION: "3.3.2" defaults: run: working-directory: ./uni diff --git a/.github/workflows/test_lint.yaml b/.github/workflows/test_lint.yaml index ffb255569..d8ef6e30e 100644 --- a/.github/workflows/test_lint.yaml +++ b/.github/workflows/test_lint.yaml @@ -14,7 +14,7 @@ jobs: java-version: '11.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.2' + flutter-version: '3.3.2' - name: Cache pub dependencies uses: actions/cache@v2 @@ -39,7 +39,7 @@ jobs: java-version: '11.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.2' + flutter-version: '3.3.2' - run: flutter pub get - run: flutter test --no-sound-null-safety diff --git a/uni/android/app/src/main/res/raw/keep.xml b/uni/android/app/src/main/res/raw/keep.xml deleted file mode 100644 index 7ebdf53a0..000000000 --- a/uni/android/app/src/main/res/raw/keep.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/uni/app_version.txt b/uni/app_version.txt index 1538c8e29..7087f1922 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.20+138 \ No newline at end of file +1.5.14+132 \ No newline at end of file diff --git a/uni/assets/images/flat_bus.png b/uni/assets/images/flat_bus.png deleted file mode 100644 index dd96f4e085dbf62b82691539c10616f97fc6d47e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29008 zcmeEu^;27K^d|03Deh2OT#E&_0>z!+?ox`o7bsFF?xnZ{r$})3QrumFOORlj&+hE( zKd?W3XL9FC-h1cfIq!SqIp-usT~z@WivkM)0RdO>ldL8J0^-eo8wMKu%GmryEPRLP zp{XE^P%}w&1V53r)>pJqQ9)pZA7daOM%yEx{5J%?QNT9@1mprl1Z4Og@xN;YNdNa+ z#G3--|9AY~KyR1H5CjBC1VvdXZ6CywTy!%dz0|w&Y;pl*-3Df0eKn0}vm#?EQ^Xr- zX+PKf`*X-{C*ZW{#koUUiU7r7$uoBM?1k9xW`TP$8|AWB)An^Yefsh6a zyU7YXM;Rn?x$bt~Yn$`w>7*8``u=0CJI8W%zVsUMQTcIX+tnLsv@H;;x>RbZJ*!~Z8>U}**jqoNEn5k~8R+TXUK;%K zpbxroYuMe+qN2LnGUDp&Q+{w;joT6L=(wUuUO+#3zj6736!Bg$qOIcz{RMJgH9?I$ zg%Cv+RGWk1N*=Ze%IZzBo+%o$onk`c);i2q-0LBbTet)(OlC{ICP1VF9G>PHH8{%R zEqwT6rAFPnNO4%7 z<}S!*^zVFlh;Plk;&{39-(WgZMdxHZunk@0esbIatqRdl$ryhZuE}A!2JXOra5v)l z-uaWIraL{){UEndTg;9-n#FlyVXn?Sx1%Kd_NF@sFj`tSox z3l@s^plc*~@w>?Fu<8~7?x0R!7i>iRB@GuUrS)8jlakG^2}35^gj&`Lh9HJgoh!)s zf0_MyaGQ`ov&#^U|BlEFC)7^DZAd$bWCVbe&Bhx0E}a7i`l=?Qe&ooXSP{kRjT>Qx zn>TcP(QOis?%28X1;aR4mHTii{Dqv0LxSLf1SPz)!%VWz*1Gcwnc?>(79@j{5(yZy z`L=L^woQ@G|5dsyE<_L&9JZ;aMU#2vZ)3XNdPhv7X4ZRLPkcA_COvx_Tgg;)3BOt~L6A@$n z&J4l980;_+h+W87uzjM2j`+K}EvEBD?pR19Ax|KrSzRD6KzWSnk=wHL=;vBwkZZ*@ zbymz3>MZZh)_(2VG9W5rsuuub`TY4R%gaBE{cYSS_yxeDINd2s5Xa+7XP`%t0maN5 z*@FWdSi5E=s<*(&3tuGC6dFj$fq8`}vWNKlyJb?Q2^75>FH{>hlBzBUND`#Fk@ZB# zl?u=C1q%K+=lQ*XSGzUF?OvN=w%6pa>`yuk_W%`=BOJEUni>}Y?Il}FnlAtw29~YD z+4m|DNqJ>i8N)NbtU*cf$XuK zgEedk?mzY`N=8jPqmIp9GY`Dvbp*KE%tkTa24`ZRyhOjfcJCeE{P$s+?~lih{c?}v zd{xHn{u71G!8s@=D@(}2j=Rgn>AUXXrQ~iaQxX!Ah5DU^yCMOf_aEo!_N}R{nOy4j zGi9m2?7dqk@29EPrOL}pv{as37x^DWg&aPJH-Wf7XpTFdnIw@W-%aPIRMHr>_HLwS zv+&(IUJc53-9pyEjFUA2jtD4h(qSXz783|^^>nknOPh4$BrIFy1Vex3jig4e-0VXATC9*r) z3C>BiJvtAWn8ZQi1qQqAjYMf>|kbBz}#RsGS4m-f)w~c zn*3HQpl=sFK-hZ^6>7=3+Q0%rIn&F8&y7_zNt%7!MZ@LHe9&OvI{wd}>0(hrH_m6S zT}|6bo7?+)d6m5PE;US|Khk2gAGu{-DA%8s$HK;{lM%DID+lTo^G zwWV?zgJfvun?f8|XI;rY$_dZwaQp4;ews#xt@>2`2PU_W5DcjI-K`%X!aLJ!yr+|H z!GQE5xOn)$(fEy1JAVOPHMF$ZhI~Bko9{Q^L539-BBn&Z_;dkjVcZ73tpjzmeLqZ@M^dwvPQs<=)UN;S1bK*YMiIqWaa+Qh85 z9xW{y#HyZDi0rd9vJ9Qfrfn`Qf187QS3wm_eq$Lu98f7#P)hT3OHx(0BOSifn3VP*dqIn z)g!D&(XdZ$u&;ONj~#JX#TNtaZcNy(q1cfwOhQmvzT;k2YqJZ=l6X)^1EK!xkqjQVG zZ~ulFD2)VP%1lWEC_+wS(~}z=z8*FF&K5rX*@Y@r|7pT)CNptTlR&7BJHL$SwEL9@ z?C8y*>UkjwWI-SJ8XoiGuPa5mLl$IYVlO@$LBT{CDO1|4myfSz$irN7c~&1Nm;mOQ ztFEpN&zzBO4(Iv8cO{76I6qe#)I)m$kh}8lSJeKZ$a_37rX}X1SQc#`Unwv`sIoSf zLX$|Lke_;An);4ePm<^qtP!?(gKOn3AKN$OpWeb(MUrZ7Dv&`t zIBTJ~+rL@q%fkJt?OOc`f6K;8zvpgEmn;jMI;KGoX_<9?K& zfKM7bD<7j6>yto|R0r3ZIw~~=I3)HA+^8I5&B>ZgMYW%DJH0^_{Z+X!D7|pw8%?}r z8y8|BsF+pYYLx}={7ji^^E}o@ylvub4euU=QqJDY{%pJYm#=I3y!`5} zP;8(qfGOawXPXXt+@RHZeSDNCGv?qJq4{|E$+EP3#FSq zxihnIsaWKCnU*L+Zw6F}yAQC;!Jx+<#verKL*Eb#hl7;@Wx%ju1&NBY)>m=p(p0dj z{U5>7%QnipQv1WSu!>rYbb?KsHD>D6QHRcs4*khjR!B*xR%DM596PK zS=l^UQXk8LC<`?_1;w~P(c5=2H37j?Qv2EIc3THttt+E17OD{%^5;w z+GIld#q59bS7DAgQ6d3)Bkg&z8Is)17Q-|_3aBZsso{At6eG(SRQZP-8FX;A);`qN zbzpk{H~HQ!8{?G*GBSOBi4yl8jI~H1qo$50pW^D{m@~y3)*c=_DsI_oIy4`?tZpHh z45h(onogkcb){+ff=QstUcov%bh;*s3YrdYb8vOlTU%R$>}R`ZiZ9~ic-}5S>c=E% zK02>c`LS-KgcM82@{9aB{)zGHm|sCx!*9JQ`eHR=5B> zc#MpuzP3BBG#?6T`_Me)yhSpSQNzAn#D;rZ&Qg}fYg|o|7bF$M;_KxK%Z_e-&74IVZum)5`4)fqwfOf+WUE zsz6l@0-2^BL&d&p+#F~W{`{PZZfmc{MHomYu| zn6lr^4~5zG4bLdp1SpvLxEPR#qA4$lv-6QEpjIQ#x2vS!0;=SD=3G(z{26Q1f)JCa zU{p-X&CB#1pNAUvz2Ntx#4OXa{b=Gp3k7H^XcBIx*m}|Kku{S80t%ZXEw_QQqSq<< zk<~n^YF+maQ}ka!8lfl)1Vv3#>R@m-NL z@VWg93oeJDJq1B`XkVteb5@?-)>Lv0#BG!&NLufWKij)-sQk8aUiSC&(DN8#Sa2X% zC$-SM0FLA8W*yk8_wn zp#;vucncoA*$P(={i%nwKv>q@SlN9JCobc5cGe2;HGeIh5DG@QOtP9p_>PbFbdV&2 zA7e1bd5o7WkEf3`_Nc$W&~JBkRGQ~S7mBPyL9q;4cXH8q+&T~i{=2_NA>*%n?qf6v zrMr3IN8j=iFnUL5qVkAwHKf{{`J3(e7j9D%bl>*>KtVl?|6hDKh@D1@C-Csj7#ntR z7f3&sH6sSGuj3xDdur9C?4yq57WD3o6z7(PB-giCcy=!01niWkC|9fu5cvog&DR*U zY91vzX;fIsw`VLYmeu@u`XV4MUK2%+(AM6jt*2MIyjJJmLnlS$M+I;U`B^_Wq%obB zN7K~ens102uVg)tqsyN_wHgfRdt>A4eFQv^qM#$K!mn$aib5 zW%_d6v_buZN;YTN7o9boCL|;z6UJ>_)hc3lW2O}O8?Z>OvhC%__E*{Nu9xLwNHM9Y zdSb8d(7bJ)0Ll~k7Hr;L5$A512Z3BB_t?R4_A04aq`JmyPQi=+Bnos*Z}+Z(#|@}q zVxy<=VM6Z=k-Xr>(!Q+O#@H&9#$t!}qvNoHC-d>~v09kJv)Z@pPP{Oq=8%k?z@qnn zO3r>l3?JOmazMZS5TD|!tOsy~Lw6zaVL%?Pn?9}7yK`WiJATUcHkfT3#R7TsLWNnuFx7^fC6KX6{CtGwL4&f)h{drXYtT$EQ_ zOM5s4C4=~dmPz>=QXxeD(2qEF+@jhlX6gvhpE8r)6=o*;cPAnOWdd_q`_K6`vE~XQ z@%kBavxn;jv)ZOC8R&H{_95E8lj7>d1@D}CAHQJSQ5L9lP)O%$gb*R-?+Uy*R7CoG z^F_bTG~7`xL!nLM?U?Q)7oJ_7#MXI?4~M$Deb(8~ut7p!9`o&!42O*jE!lA2ZDa6j zE#&Y-b~8Posk8a-#cH#5;aH(Xbyb;qx~X5LgQIi&2-zt9m?sY%W&cSj_MV(F^S4*d z3K*qJr||LzdtuIozX@x(gWm(zmIcU*OcKyb#*M7e*bHNu%r++oNFFz*lM3D6hbik3 z?(^P%PqUI_rFc*NdyrweE_Lp6JszjD$U=U2HobJ1#+i-rzZa4sW{+QlTUFnpnM}0} zAHT?`Oo(xup5n^`;?SKL;qC?H@*z2`ww|{TAaPWrRTvf@^kvue3pbx|TY1ULN$zR; znZ8@>!sC=5-NiSmq4=jCqvhrQF1$uU!yn9shnz}%=JOxJIyl4$DlAC1Ix6ZJCW-h3 z5StoyEp><{LICga#ZI_&j5M8eniUTkN=r$P_X5!v%~PEfwNG-5*OX99Np8(S5jM=k zL>63TAv4uzXlQc|WixEtN#B`A3V=bSg~*q73nXYZR)QTWCJs48$1;Dt#t*X&Yzg*b z#Iu?Ft+swRx6~%sDlx;|P3@UH4(!4=34Ln}{@V2dO2(?qApKOt(`?BLgyds)F_wjj#VJOv@nZyB2f*xr#e!j%)3*Ih0K}O z;-)ztPIfQOPEkBiW^@k`i~NIOm7`#c2RoW~jP!6x-(`iiQ-!>jbtUSYx!q+l-#&BM zu6Fb8 z#?t!StKT;74y6$++6iZLNGwy&%{>hp%5}F>)}eNhzbZa z1nBF(KTaIOMgFtAEaFK}btE^ualb%08+f-`DSme}Mb>M~kKw;rCX2H_^P@fxysBO& zL&6aT!jjJ-8WJZ?Zd7(cKtq!o=)_{Z?O(QDFD)Fb8oBCuw$FqhqjjY1r8>3O@A+Zf zI2|34E!&HeoqTy@Mbey&`$aVLqJt8w7yw|X808ASRIaG1!qL6dg7l!%WDrAUv}2rK z(j{IE0{0v8bp#rKvrHdhcdtkJv$O?Dq!ZoVUC-2!;1bC?f_E09#G(3<>2=q zUX}HQap+b536{m7Q*=JBDT#67lB3OVRI3+ip~cWlPd!r_-se#9_|TI#jcToA@42l7 z;f_s1`|E)r*E?%3zZgE96LF2YzB*;zUL)4$)n?}?>&It&vEM=0w6BFVuu1QN<-)0b@&`ScaDS$?5ZP6$eScZic7TJv zo4k#gY%MB2#fBWy6Cw1%+#iAX*z0yQAR^Sdtxent z&jrqtwcMDTcfCSfXs+fAyN-21!3WgAK=m7@`RNHjv-4WTRbA(YyqyzOF)+B6vIv5V_|Ib-i&kJ9&f8f-7A2=_|AZfbfK~7xZV}K`c(aOvqdv>Blezw zfD*ffMWZfGuxkwi#yJ0Afo(n{Mi-TTm25@P*p^FuoKFQuBxNU^M=u*6WvRg!iS{7o zH#v!aG}ir&cQT=?FK^uQd<#gEw)37AhxTOqMT=@yR{5Dbq5W*xu~3ss9sEswKE7%B zf(f`LTMz(?Wt$5cr;xadoQlr3SUT`M-dhAxADi{pWBnE`YTYBdF+fIX{g5$ZRQ)?n zFmQa#OzwoPKeaAb@x)lZuzVY5O?C8aBpYc>EYR9fWdc16KxZZ4-2t~=?Sx^CZsbqu zGR3)v-({;hp2-OA2XvTeV&j-&YH!DaU%jEHu0j76Z8=$it>T(X+Y$RCEP33wzy0YN z2ZT5Q;7i!?;5sxuPS zwCU9NM9eA?#?Mt7L4+YhNwUrSu?zgREbgki0x+?gmX8OPGA|G zX=#5qn*Pc3lW83N&P^k2jqeb_pr=6ZpOVR;j#keJsw9>fG?NrzlwT6QyruC@ia03a z=+XeBwZ_kQOovP|HWU741n=ei*pt^B9^JqVqC zV}8&ed99NxZ$wSG*DIApk#cgilLAd(qX-UgYg>h(%u1aBM1!LKLB{Azhd^HwbirgU zp5pnQD6f=KJUVJHRlxe_TF|S*-cEZ}{ngGhCrV!Gc0xIMuVR|qztOP@;Ia_8hK(>qIY} zz4_K0`pc~)ZLGztixF&2eUZFw#H7uE+@F3X6>N7lk#s`$eBQCt5fbeDLHpxn?DQ~z zg;t2{#^c0ssUV2c^b4@R<%E<|DB2#Zso9Q67K3W5aJQeeYJocT+3|WXeCO@{6f}{MO9Q4` z)Y-|4qT;DEElMF{P$f2L_V@plvp z7Y3DAg!{y-#IND-7Syr9UqasK=D||0J=VGt51tJ>n@&))ZqlTmw$|mW&}&b@_-e3_ ze!t)KF!joPo6~hQ$_AO!Gl^t=E?|vDz3M>^CX{|M)4^@2p?$ni__;T&tQHIn*Kp(Ky1JKD$L#J zcOwqH(VgWyComz+{5aFtJ|JLqF-yOE7KHem1tqR#tP3UxJYpOLj*B!woMVmitv^gsaKP)GQ%Bi(J2>%x=Humz^ zyj#KFaB}&WCerWP^w(Ymml8;kPrT+g*vc-;3OKhbk+4Kh~iMQgH6-$u(fbkHnjJ3f94IiAaH z*>JILAl+s52eGj1c;qzD5l2(QV>nJSku<66X?@dDD|NefJmPPnr9NXxMV#<`=WnUW zyyNk;<$}jDC#R3CY1ypa9>3w%OV0JA(Lu1%XN~sNE<)ycWGbk_<#C1DVTf{NP8cJl z4Z37Ip|$_KhU%SZftnu;yV7R%`z+IDoJtBtTUz3q%wY1Q?Ool3Y=SBO`5=Ey|i-Y!6B&Qx&JBn3>$m>TM>zY zUK3x;w9jp(?d;0>?Z+ZA%pZQ&r!D2)xxP_^9ebn_U4D5QX|jg?ToJy=?6rFxhJPYc z#STSmeyT}8f9L85%tM?x=uk7qR8ie?82Q}XS+$J-4JI@ujaB)469fcB-4~}Rx^+CL zZN9X?RIY1xS~&i9(9Au7kMI@vwn3MKVZGrbr})T6T^yD6)MD|rEBGF{W8sv)f-~)G zqXWVU&3}Csg{DiGf;fX-(Q$O1nu1{|uzTk83VgGPr>u<^FV4|)pLAkG&%W0u6Wp%L zHk}F6q^aGX?RO+G(Sq(7T85fVe`+Tt#0o6%U8|f{EJP%`YxdrHaKV_KWKe9L6{+5* zp4Z19RIfIaw-hzby*b?fBr$4_Nu40Ls8khs5_~E!7dS)>Y4ShU*df?opVNsNy>m2u zsAxNy2ky^yxk?-{c^<669onD=Hi&U)o{bRf{($yH9ej>_p3kNvUH&q4{<8AzN~f+x zFYvh`UyD|HGb!t?Q%@&1FyYG3?pa)5PgTw2mqj{;JMVFgRK%a;?QP+pzw4jd8>{~= zr<`1t+xwreNS^~W+DVH*8TBa$wuehg=N{F0802%&;$-`(pRVFlk(`jkyS<;!x@L(< zb3l1vl0m3^=Rl9^&A#O#$RJ@g_Iy9cV(P&&xXsI?df-d2u}VB)mX>s?%W9+h`kln{ zh#C$@Sk)~J9wR5M_WcUbaMp3-G0$%|x3rvE;2YLYg=tZCa(=ylrTL8igao}NR_EtLo;&NQ8DGJ%w}EMQdk zHRcEN4;wIv*J_$RzbD6y=NgLRL+%6?SR#hU(%^2|Y*S45kog3Pi90Ijs9;z5VDe&v%K~qNhHu;p zp=$f)Z-x2sg=%9KvnJ{5GQSri*kVOs&`vYYJ_h76#{Q zr>i*&E7})pOkh<$Hq0pmG{!A!B_efb;%MeX9AZGjk}JaS=Wow^?qz!bd^AM#M4U>! zG*U`Fv!$@xI{aXLv4{Q?kKWaBHJf4Q4A0KHUvZlYfrEy?PEkKEkQ-uz0U;Wy|Miu& zb%uF5*_LMyZzm;itzt?{O~A-Kk~IW-$tSrV&HD=x$k!PInY9i)5G%&TC3prI{CDnd zdp3hzVov9vz#1hM-jA8I##DIMKyA&VX8T|E*6VfAM3k`rS?Ndg9oGFuvj~SUTPWR` z|F(P6BPdwMpVn74rrU@ex7S_K!@SVX)q5fA-$(lQ!*K)XMwQ~1_rw~uG4lM}RoG+f znthe(5k8wCv47q+KtKBYz$QJkDHK5oI^YnjbUE=gMc!$RQZp&MB4FT8Yh+5%=dC-U zy6o#LEgiZLGl`!D<)KuUzLRc_QCbd#L7HhkMF#oVEb2#^(mLVK0g+P5Wbt;`_>nS zYeX|~f-0wJi;6wx{HF+t#WsPn?EpV@utwu5!7@}G*Ps56%s+GWK5v<4GDo5{EPa>$&@2txdd^$*(ivGA+R6OuUwtaANr=Fym zdN7E$fSBzTz%Ia)EhLlpSsiAwjjo?NzQ=nV)GQLbe}D%!MQ4XBD-Obt+y~4Id=xh9 z*k*iRpCLSfT4$iN95wbr)vqtEQ6#V(>%AyzzYjFY#pPHy4*Da} z_&BoDps)>SXO2-{toZzBf@fo8=MarfYAKix!t6CM^Xu)OVNxQ1-1e+8f33hTi+lkB zquu0;&M%sjmKAQXs`YVkM|dAX!__@fWAa>Xf|g zEFzXjiY@K*YB}2A!ZD*-kP*e9lBI6CR1;gHdP7~OI&0A8$~d7Xl!^R38UC;kRo=+5 z-@?XAbD1P5TR>e~i;@pTYWy{=M%}sm_giyz!m8>TU2E%N_x;-YBh`P>>?SL-fsX0> zy^UY*s@7IC>VT;KHSkw$oxU zM-uH2Sbk;YMhCJrlj1r)@U8uIKIpWu`nF{jTDWm11}n>wXKKlj>nM-W$RnvZo&I7O z$hGCPQectpY**g`kdW{t@wZ#DNZ%|0-xhkdJO(yOxzz7Pac0K%9jbcSb6dkL_RY!h zb?e)>bKBP{k_HaG8;q5&w-%yHH)a-vOmf}g1|}tbThx;^{Cs8^20KQgJa`~?zQ=`~ z4{B%}Vs~q$^Sv-J!{i=S5fqqsMCK`Ey+~;@&e&$(%MMdWG;#iz`)%V0b&ar0db0H~ z1*+eT`0%c)-Co`ZxLtN-e5p0(Elg5>yFQ~U;l@mG_LS#<+v67wfF^?E#TnzTeJM9S zD6KFSWKxvJ$`&VjIG|~W(Qf0g<&5#%L7Vopc2U6MxF_Jau*@)C3~0XvMmG;spEIrz z>ozjqyx@XC)x+7ofBv&l6fmq5i%g7BwsMr?lVSf}i*bRQL6E)<`HoIq{#zFWrq0DX zu?MRY^38G8vbk5RLYR%YhS9>FW&C0+9SN^QFrxp8Yej|TGj~#)Kul?SOiiT!-o!B@ zheF1N!Hf62Nl&74V7z)=x@Cxkt&;u9iPJY+-?-C=MRomLX#uCgJI{_iNlW4`B~~SR z8V)l96OGu-EpGTyF_+Z5dASY67B~qW#D!kt#0|xVYF_FoXHgiMQ6VwN*4{SR&Mx~sb)fi&%nkvd2Iu@S>-nBvusQ=?C8--3O?|D`%24vi(S z1YA-reCx2b!Hth1{Bf!-l+V*rqDarMYi&jca%L`uFk_qypi$Pp8lzD}_aq16=hS{O zwgwwR=)*C2_yeZ$T#5-uRJWT+U6&gRIJ_pOS-Kml!F?BDEV?J$wx;Txx#%ZE`d;(G zslL%0l)9-LhRth>V6*jwn0?AwWQkjNa%=}CE7)Wi1v1UGd+6YFM~cBn=~v!KMGMt4 z4fL$&?0i8sRNufVJc~}5$#_JR=KeO+aQ3$eU$Es=Y1}BFnRc9V!@;xN3hob|ckFea zJXNjdEm26epwqwaH4dniKtFi5O=ok*;9#y%Jyr#@*|s4PtUXY!`igNHU1+a=hA+8@ zNA6;_mqUyOtDCFtCOBkpOAMV({3%Otcd%ck!c7Cj>pD}DlH-sT(exvde)c>GO+OzX zIfd7PZZ6uux}AJ(8F&)nn5M01|4{q?MJF&TAucpeCnX_216I?N$ypEvjdO;Qm?Qut zRs=Hm&MmJyRew&^+}=`VJ2<+^{S5a=Qs@9G4Z2TYPdGcjIgIg=Bw83?|CIbbJYAly z)^VXGox;&!y~Hu^_`;LJg!c$I&hop;@rYnQyp85*I2)du+xE~L`95w{0ZATZKiQs+ zh6k0qd5`DVqL%U*U9p~5Vi|;Yy%P{7C%ZwqRb}p-R1D4fYUNuck-sZm5?!nELi& zUe~1JUzxQ?7pTR?QbP&MmlxGRX-k`I0aANKRJF?bavvsBqe&Oj^=GmE%>*KK*&CQ7 z7yp(`u_JUpZ7EQEV)Xi#v;B9DC! z;<*!ra1#vuYMJQV?htAHH)QktC%d9;_e*xL=OGT5ub*rZpd3(_Xz}iO)7V=MX9YGL zj$d)m?4l)_?OmZj6Gayt3!{p-{mzmqV>X%v1M35g&{=pa0JiTmQsY`$bIXQ)WEhN% z12mVvW%^)0<>uceq|9Bie~t zkV#bAs}RN)KE-cy-;l(d+|2W6r%W?{oJY<7!gx9TFKlV$kOg@D@HqXFL*y!!6VPcK zPIE2(mPb5cqa^}G^`lhEd(khu&}^&gZRjoWw-V{PE~~U8T_lF-{u!g>2fEsN>5}8A zy5^4p_r1MRkE;5$rWbj4vTyb^6It?oG}rv6#2|a5Cj6`4^46Eitp=&olF?V*R+S%E zDe`&G(H>Gqey2pu>qhFIpj$kfC+wUi{rY*b2ErUR5o^2S&nR-71QP; zb_CX+0@{ssqSlA@&BRs~ZT$T|OgG$+5;zE1RzRwx&CzIoJhi!mU(tVtvN)XKbxej= zh#A86bv88b0B&8d1=APlJsD4Nq*Nb(BFbGj8ygN{5SGoDh`2HxDP}Ljo@oTshR5nU za*jijocBJn-&f-PFaF$)npn9Dcn7blJY8Ny2Yh|=PY2;)EUP2as)`=9Rsyj>b;K&I~8^gmu;gt>L0fBvS>U61SJ!;91zpw4nL#*k^j{8JQ=ar7< z;-Cs-`z}~=Bw6w|ljAnL6e`PsBjq%4Bvv_YJbvZFPv}-RwCbo!=C$d zywh7VH%C8R7TqJRiTOqx58OrC9<^cp7OrQ;AyXMZV!u8;Z9_`?2_wdnVAu!dGn2l5 zG_uN~TfbW<-|lQ}jM!%K>w(I3nbqS3%}yVM$SMbj@vgjl(j7x$f%6mA@|?iGvPp3} znX(Hc*5@$7vgk79xVZGbEC4K_Cq-3E4$5$^Xh-xm1q?2X-oEwaRJ9QC@~#+-#nLea z9%GO^cA#yAFWqeoJ@|@;%A$E(d+6VnzX{(pTJ-#sEXAR9OIV;`bPH|$Yr!{~6~Il4 zq+7^Ms~goOCS4--&Gh9GLw94pJet=15B)T#HxH6)E{TEE09D5WEY!?iCek>ep;vGRANQAECy>~d`0XXZ{=#$rd;)xJ_$*`Qm9HJ?0 zW|)QWY`b#|GDX=DeX^zq(*_JRb%%i|S>4=?{zdDd(|BEh$2URI#V|_sfuZY7(Ko=w zsAgBabD=F+&zY43z1N48*Khg_B;5Te0W3TS7%CR0G6&JrOC9t-_L+u8a~~ zOlCta2oD^m`OrdwIOpMB!^>|g@bcq!FgkHnupLwVLB6S7WS&{`p!F}WwQ%r()OZ*p zGgpKb-t6X&8A1iBLUFI9V#q}wUbrYgrMg>&HH{ykua%OJP(?a-km2`TA44EE8i}{$ zpJ+}AcHK0eOtvtsY*w7NlYnD(&lC3{Ie-|t=Z~-5m5<~0ePlQ|=Zieo=C>Q4H8ob6 zy8LKraUKYcB{}o#?=~aB zG{JagdaZjyo!9KzV$J07NoH{*__W-l@VkXR7{xJvJ^YvP*Cp`Q04nG-AabzkanPx} zKS39kEW7OQvhNFE$X<(@{^dAdtpm^PhZ>^NucL;8s55%R;Tnj{>`6|?GVmd{U36Ue z*9(ahI|ORiqy5W->?(4XUiXqz3e1&u8CPs+Ch*D>0cGKA$3ad^nIq3AcU^Sj&Kct` zqbC~`yBcPMm8GOfbb{yB0IYXngZv{Q8I_@R z7lWvbOY5Eu@`#jMa^vjz;>3ym-60(6qK1)sSEiu*%8P7qq|n<5&}&ge*eGncC_$-W zZCkzM*vm;Lj&6MX`v7zCpAXM#!7u6X3SAHnKBewC!?2pvC#{&Iw{d^7P6YBXfR%Y; z+zEo=>M3#9`k19{`pHXEEB=1k#5kuWZRbJI2?ZLRdM~GU8ZD1K1@0#ydLq}N527(W zpI^1q%1MT3U8yFS=ra_Mimj=}6L`t;LV6Doo5+_nxu9RXb91f!OmFiEERd)>DG zbs$tNopQmXj6`_87v?&x0b#jhcS18_I4 zwkIY&(>O_JHB{JN>mP0$Y*zsfoBpm#VBt{~3Dy0rd&jvlwa-j;FQ>FZh~?!YmGm>y z!`m!I22G5@=9xrMirUoi74^#)*v`nU^!IEX-#hTV6g& z2k4mXN7gQR+r3kmsVb|#HLyySeQ@czPxS4He{}WM#&|5t0aD{aLHYcKAx19kSA(pT zjh{qBW?A0@$NC38qkbh*QiB!{PK_{SrUyw7_J{RLH7sz0D=O)IKp? zKPtZq6Ca)DlRrWO)TBx+|NC^Ij$7gAt_u<`_s;D2THyPp^HDDK#47zl);jHX4Obl( z%yf5xKR7;x!Kn(O3tp(E@28x(6-y!eR@$JxMCwDm=n21q(bL|d;Zm` z{P=@JgqMNLUwWqxIaUbG^V_d^uR-&cDAkEs`!dn&RAqi~BXr3RAf;*aS6w$5g-TUY z8gZ=3$cQ{Y%-}(4x`5{gywPX1`E3kV;sjw*^w7h%jCVA1ExHK;HDVk@=+M6K8W-9uzuk5?-fauVB=2R!TUs2E0LU9TngF0fTMy zzs4#d#27`oyPpwA;-u}LE8e=lN%8s6!JC4q{vR>rXt~nQHs@bk!>KXp5_UYJA6T%W z&upWDR0Ee%;;MqypL`|=ZDgxsJ8v~fsiYcPkT)r(6dh-GUn!Bi2T`I?^Bgz4`K(_- z@{F=In5iagRSl>oKzXNqUzST` z%%l)0N$uk=py8J*Z%c5;JEaJTZH7=E47Vs6e2%>K_lQdt9`xE#$emQ4*xKJ@?#JLD zaOa1tl70!s*#=dmYh_Fw^r^+bnR&GwYS4Gd0-X8tt0dN57;J+3>PVk}7|<&RkVP_( zmOMjm<+92(I!n=9<@(HIMgq_HRes{l7e=yiOAl9wnlnzVoQ)e`_Sm?@#k68WY{fqt ze5NDBm7B;L`((ca)2gjrGm>e+b@s9ip!+k{NT~mdOK`|dNv0_OsgzuQ-J4>(#IDZ%^91?X{|6}e{5m9uT1QJu!oYC9vz1}(-Qpx<=s+U$ht<-__3}} z9&vxy54<@n|D9WGt>{MTF1;fV)~?O-M>*{UufBSTujhZY_f=7GEkV1146cFT?hxD( z++BmaYj7v{3@!nJGlU@!f@_e$-JK-3O9(y?2oN-PbI$qS?%Q4KKK*O$hnd+k-LPYXNCbSn_QP~LHU**S?>^+l};;a9vNOB12%1z%c}}?9&9^$ zi0(5$i9~NQ`4ufgJ+df$MX;lUBA|VJueg|a5EtdQ29u};(!<{Ymz@QVob_V|5~x;} zyt!)p9X+x>Y}!Wjn3=8>7@3D$-;bWWC8yq4d%FD!^PJrwJP5ZDB z^m+9;WGhjUSR@4R*Q@SA4H%k7uj@53Oc${Ag`c7WGZ_iC2SDvTW_j#vT6nZd1Dyq? zzQ2Kjp^zgr#tmC!0)?H$Z+sVGp>0FB*rk0}c)~5Ml_tc%*B<1kYG$Kv`o;@9`sPt;RD+SFbub}?Mn)T95R>YmEvi4+I1~pF-UT!(fGk_zHFb|& zz12`Xa&rTe^4@E98$Efq!Q~-i1M=@_WsGPOstBv=c&Xq)bjOu&|6RehhX82u@?&J- zk|qREim(~e9iGsZzA=wHNnn%=K-f_vgp9$ z>8nkE{8L<|PZs7&09om%(&cBfF{6D7sOr@8tvH+mqk@k1ax)?q7k zj~UlOy`t~VF5R$v2Kg^f?3^gchz3EUQ1`Po!mc9{YgO{7fTh-Olp!gw<`60Euk_yp zJP~ax23loqy?qz&IywgP)-eppo2=$$fbv(y@>y0fUljcE)Hu)<-D`a8Qo9E*!7UE& zcx8X6*ELZbeiuoXuyE+ht*l)%BdH!`TqfTQAu+%dd-@P zdDwM?to{)K-9@H)m!@h)y7^f!JniGfMh`j-3PcJT9ia7|=#N~|GDNpHVu=5RT6cT3 zSv;qc-&WRC*IrGqQbdYpqI_?tBT4a4H?Q{YJBE7ZU#?1*PN3=)c`AaYMju`P0^kjUSl>=IPYw+f9TDX#B9SD)}Gw2bzG0+bh@&n z>iDRUbfhqO#|e~wx*^HgTX91SGPw=ht;KcTZN8}Wo0LN?qos}Z9HOn3+FrO}^S!zTr!e5ELqXg zElnaLeK+#$&yAq-Y-|*sDVv44Us51bn6e)t4{!44M+`f|Rg&2S1Zx{pri$PKxl#Q6 z9>MM1`^|e**yM>aEhjLH-@{>1gEILP3La^%TN3J%4Zrb3227(4@j*8<`YDC_dwIWl zYimW%hVVX*+3gXjEK0aF!2B&2M%O;+hOCeG_PCn+e2N}9yZYUIz7&I?l*=?MIqqC>_LBbM2d`*9Uq|mh37DvxSn2v*|HVf*r(Aqa%{i4 zA`A2(9x{*v6wp30GfC+pH+Ut-GxK-7$2bv~dt%4=8qPoBBHJg8_tJ4BbeXcb-RGQD z6HdJI2}l1k@5lMtI_~xlPt^cIc?Qo85Thzb1(y{~kVLb6R1ohX7%UuvXmU@!q?xOZ&_wne z5OGrGH#ah`G&eD~RW|ptqt7F__4b~5k5@zakz66$z^E^)XP6OT=c&jZajvACbmLL; zlRP}+XSK!pw8!tH%uELc*pv+ds02kkNW5DJf21(r(f;f0#`9WFRH9n1*R@d65s?Pr~E3EVfmW#YSruL#xN>KQ-F@IHZw?A5wWv) zYZTA#a5#AXF32dd^QbATu#{Ek^KG6}dD-pS(q(Jd+9&uzNnKq1s@N(yU_x^qKH6Ny zHm=867x@oq7YqzHznomt{oI)FP1%a|k<0N70zwdYTKq7&kD(}iCm&_dI z%9j$BmX?lmBnOhf4#sl86r_KzFZ|W;)HqgeiAVfC4>FT-e$4j!{t1#RMQb@pLGzIaODa z_K^`)IoZG0Otd^_YNxKWZPX{A6UWEPE8{7?l2Wv1TVs%u>q#ci!NGxwcf`#)txvK@ zlY^$Y@QinvT3ymSa#QxyHWE{``G?Juo6;U>S*mPDaYo$6%FQmSuhrJdU1e1XI|6yK z$lf5^_kl)=w=26vYs2nG9jj)89D!xtg(2Ma9%N5WqK!kghhs%RerQa!TFQ0=t`w1h z`RrYmdG3-(yNJFZkY1pY7ugApvz?RC-B+* zJ;#kKA|s(q#C0{G*09Z%*4xj$=4z9OT}(8KXf4UE^_+EvgyCn>3G4LgS4}PFIB0-G zL+f~|$<^9||-B^#!3+EW*_&&+k56}nXuvCohO^xWmuFHN+ zaPe=U>Fbw<)Jh9`yVux>^3q@UR}Mw)0^8yoY9?E3K`g3--(h zH@>7tB1|JsOd|s){$%4lu)4WAI+l8>tQsc&0anhTsc~Kdfw@_;a90N!@rtGr+ExB@ z$Je-ngyijf{~jd~ zwlO#H;nl9ay}NB_1?#lg(on5q%_-<2O!73Ah2_J(;_rRT(TZJrGl6yH34|NuH(SA!w^c89 z{z|kry`fNyHv6I@JtzLFo2NZ1-Le))xm+DsT|vAHK7pw)-Wc4I ztM-QWKcb0=iCyk)ZV2B%m{Hd?uvG4zdkmBe9t`?QbDqYfT^^q|%j8UHXEgQ^-rUaG zyzWSt@~x;^AJ~QoP2$_ou%`-5JfnFV*5p|x1u9@%9JMt_n&dW8GIKd(b#&`CIl8L|8vO3yr^itOXU5gehHH_}X zcCOg`l`b0163xt}JQ*`XrKjMg=Vi~Q-$wW$3ri?zREIZV_@ z-tcnRX2epkeYj(yTnwt|g#J3eB)UGM=xzRM37_NsNatIPcM%%vZ6rb9a~xS=_Dvpc z0woe8fU(F~7ukz!l_7?ZSq`|g~uCv+A`Ud|MCjce6Jn2SqR0PRmjt^QFxKiv1l zLVN_iC(f;$5%ihX3nIHZY=YXfZmfX;C(z-~I*yG?-RJ0*!Jn$UgGB499-i-U9eS?V z3N8du*Locbo1x>zw0+k$Y){xG11PNck>V^)DYr;-W3$hp9`3}~B^^1%-$&k!g{BL{ zdmF{Pk#?oRb{+fz7u)8avAe*ls;*4#3R#!%Rn^OIlI>@U6Ua~_$k@d?&2Y^iYG_8K zg4p0|6)t7;8?-Zuj^Q)(-^bRABN}U{2%1cqRgL_ZzRZQ8w2>}l8rdjIqBC2=+~re! zz2mayVlQ^plm;Ha4w|psha=&(Iuw8{%`N9BT0nPWwvVutGh(keg1!x$+_uJ5e}?Bu zds~@uE1>E#s_(0yQt9uIlxb)?D8n-$cgJoq4M>AIaJcL!{x4@Db}MW7)YB3I;p6*S zr>S&Cg$hy{16!4#x+1Da)w~g##teSpm@@38u$??;d~-4DJX3MX=#So8&7AWC31^5j z)7?arOk)37rMRdIX19Wa#!~b>^x|b{#IZ_{cZD7yeHB%_qoW_w*DaS`_>e*ZeQQTu zyI}`vR{sbinbDY~4?k+rmI<9sDqb=vwnLRxlr83Sl7>F=kTfjv95%ff-w-G_X@is3 zCOSvmPu9czsf={eNm)uQHvMprHEr$FpveJ?+Z57td+k&hYqsELF1QPGkE)G{kgO)d zU4Q)L^Wi;VvEzveetU{K{hVsX%tMM=vl3*d-*40UTSCeBnVVs{iuA?8z`h&?ZuVcr zlx^-iUCzL7{Kfp`+A-Q+JZ{Ww);P4?_q|o%eL1nLK2k+n`~)03w6JM;(kRS zGoG3#k-w{3T_61QTx>!GO*h3~zgi_I%HcADr0mMyw~U~FFo|WyQo#dczrTD6e%{#j zw$hp&iG^?>X|aW7GRqFilES#WlE=oW`a@f_W)a7~gS(K6=3oWqHWE70dxUMqLAhhp z_u;Gd)}`G`O`ZhDhXf!ie__dK|lS5B8iLk6_BYv*odhDc*Y3 z==lk%yGMz>qolm%%{p0l54h zsiJ;i31Mgj2+XasZ?=AmXS^-9`}k*hGi$k;v0hBMZpG3dkeob?J`6u|A+9Xirffd* zETi1sni9FEGi2dPU-!kA#?rx1clOjuGyms|FaHETyN6?B9>?LLx6)0^>Y(`1n_e$Y zMN`*Z+&@R8ctEI)^RK~1#HKGbDM$lx*F}>70*dn++h63;t6aAsu)ND3yJe{M=6EGGXQC>JZ$lbRd2d z?j|jK#DPUy%!f7X)iOVR%b6iRq^EyXiPq4}l_P%5`MM2n!%7qal>jUu5ge&drTB!H zjFBnT_P*nlOLU%+1l*io^bNZG0wo~4ZywCSW9*nhv z#OJ4V#+Kv1nHG3uMUQfN5z2#pc`HC;5{AU-y-V?DkdW-nKBAR-gz zp*jx~;_uUcbZqgcbxCYiX+ZvB!NQ*+hti6E?nNpU6vz zMY1d9S4_PSDoDarUypvFGU>%Yo&$?=QJ)@?ZV+LzE;4`B*2r$0r%JV+wgI0kI&0j% zrPV|DSvGQNV)`skFF`$#zhjD45Hg)07->wS3{@>qFYGIo?SDETjvK{<#-f5+ZK`Be zcA-5+8ee#ysJ#fUUjgzuU%pF<{DT)uF_21fXLT&%OCL=ASyV{X968B$d@wFrT7X$Y z6O9??5?25=+2eZQ{lGouD(70wEf?@4loW5p{bg2`+U2K*JinekcIeA9V}jTiFvZ+d zkvKmjA|vNgeMsU{5*lc#@sUx5)9`rgxq>HBzp;0e6bah2I68UXV+2+L3AZbCL{o!7cMOFfM$xLt9 zYrb?PDymsCqe|Mk(ak@Vz?|K^FkCS_Ci9dXFEBS!KY?}%nsSc9Dj)JsX3h%2ciaxyCQfubp zR|XWy*{u}|hvRCzsZ^TEN~cx$&1b`&OLQ$i8nHY_uN4=p2Ct%uc68ofgA=#-snYn) zVu$$U*Sjq8Vg4>^o8xu(tg-$ho4Ku>o+n;)r?MB*c;xM1{CaX0k)S;_qa`Pj)^S4o zIwu=X(00{oOB(VjCoDQpJHM^8{$07#jlj%@Vuf*{Mv^JR>hfCl(z;YPdRjQ6TPvEq z+17zcx(dhV+z%ow5eF&|MC;^lHE5wjldjpEINBy%LL8_rLyFJ1+=qvdla_h^`Okgu zM>#0KfrM->!X^K^u&kQ^vGVs}ukauv%RyLq^dhF_Y zP;X$G!YA-HiNlyU*Pv&{Y!|1Em1#Fb8G>k+Fltc9Bg?pL_ZG7nZBI#g#>CB1q}v}x zELcDaz0Q1VI4~+dCq96A-zw;A)4_;1AJmHej0nCR#UJ~N5L;c)C-z6NX_tami5qCg zZ*UConm_ydhLCmpWM}7KzYbuqP4bBPeX?cE*(;AhZ9Ix{CV<*z27_Vzbepg%Fj3tx z|0a;g@8djO%t3lK>+= z_dl-nIEkS9Zek_uZV>qN_9qk7yFVv>MOk#VOv8zKbDJp#EkwP+7c-xYoxkEnDA^55 z+yu&rRLBYIf7bZJzBi(pSdgPhqBuOMQ9v%xcbduXuUleFT56!# zud6%`HfI?5?E~<6PaINL9iJT#+fFV$Jrfb}~a>}COWEcj1 zsi)Fp5U9SJt0K?-^Q3;crTifRA%o>B_GzK?a+I!_lKp zgg_#Q&&#Acg&1Pe1D+H!?>GGR{_LDN8IyKSn;#gWqjmY{SU4*&>**m4`1la>&5fYn zdp(eVg7|>~0!l$o9C6OB%zmsQ0Gu&1uI?8hsa)B`@-ey%^mk9TCf=H)NVGEwmf0mXLA zE{Q3UsZ7bkDd2jsVqS^0v>(o2xrag{AM*OgRmDYi#`AphGPSKm*I|c5a=lC31ZgNN z+Wcw#@-?8DP^?epaNvk;n6{qhFv!@Q$wksPniP)Aq)Mm%+3k-axUth4gzQ36glQ(G?a}h>F30hwNPUr15ObQYd8^NX;A-9P-9lu_D2}Gjc1&y5B zaUK~#j>>vzgevBX+5OhnvpaLD&%ac{MWNEhF6?MFxfWqT7I89MF`^k zLu?~?Zo!bsDBZ3!L>y93FDRlI(-1dHi$vf-jMT@P1k4{_73{`MHEvX;BoTRov|5Ra z&!cqFg-es8K)m#GB$$TWzkEd}eCeAEP-idz4IdBAE?}4-ulk%XMJx>?kB&{x3mYy9H z`+`FC3OY#^2pX*WI!Ia&UpE~dS%lL!?_Zkij8zBAib|(bUYT3U07A^0x642M49%iF zHkb5%Sisg9D3$@T%kQ2DEub>;QoTP%I-er5B)5RO-<#|5k8z`-kr&AzLAUH({&EJh zLqz?;zOrU^?9&v}qX;;AT!QJF2t9}={j4|WcE5EJS&=U9FP}QxOB3dB!8qND)MFo7&?WNVY5yAf@I4+L%BNzWJ_UT+atzaDz9X7 z)guHFh`e|XYL+TYP||+?eyyzR0YiPcDd@_L zyvuGFFCJN3R>TWV)^mAziPWBD;O+RG-1(5Zn{$o!RZHm#vFaIqccNSAk4u}An$OcQ zQNc+P`1n?MvyzdWj!$iQEJI&DOx}tWGQAi71^|kF$Y<=tR_wh23NZ1CL0qEoKcQ-! zL%dr4W7ogXe-4mx`neyfbQQF8Tp$Ljcm4khy{jo>5eFG&6J?X35BH5%MB9fMeZdB) zm9X$5|LvBtowdQ%)~ipSPI^Rc0>wbK_rYIu6S=X7MSGOjNAPr3 z3MR=wLS?taX_QMI5>No}%z}{c zL8S;|P4#Ll@86W9a#D6#z-M59ig|M%LcN!(Y4UmbqOH6zkZM#9Mw})I99>*0e}gWD zet9w!JmNKA9FQDWuC}@H78NOZ;6n5rv`8IP`n3V0(rob{^lumaiE-PRL+2yIDUYnw zt%8p0v59h@I1rCp{cfOQznk-o%TFuANbk`g2)c3gkG&C9p_j9{$gA=pNsHIZT4r2n z&81U@dITPVNb0bogBu5<9kyw+na!Lt-ImhU$`^nPw-~3;?N1V)rTn!V_H#74khl)G z^)o$L(VEdx7=mG@W3jZjbi**_Z66IMn0A?fL6?=KoJ*_3 z#|j&+^51Xg$FDm&SdD)GDbW^Us1^9)^(gFO?3~|qwzRY`6qN!AVh;Vade} z6#V&YF313~7>M5RvZ-;dq{K1Pi3mu3sG=VU{&&cgXcGaQeMkQ&|38DO2fY9Pdq4OM z#|L=_F{ERMR)gCq!<;gd-xh2|UHqBe0l1H+{c159ZSzLK%)mDf-GnON_4~cJrs#k~ z8vqB8P1qe&l$yVSHykZJ8E?zU2HoHvb)ev z1H9|6rwd{4jznDnCiGOWFUaR2h;zvQ=Oj#&rD`l)I7Mm;_%@-#5WDL?MJx`%*8%7# z6=5_7SD_T~mYIz2IUprI;%@`JgAR+H{~Y=B<3gVR;A_YbfMzj)9*Z;Hz3U7)8(|5n zv6-(t`k6etwtoj7xQ!C1@B4~E z^&f+LUj{q?f1|@9*FW|HseD9*^;Rba|203`7l6}LJ2rwR{9~T96~F*E==s-w`PaIk z0Awz%6O?%jz>rdv(#C`>SCUtZmdo^R}BWu2EYjX6M-RD_}PxtZeGLS zu8=$!6L}uWS*g{Ev8Up{c3&=BtFez@CCLQRW_mTSYR-V+zx8loy?dJ@dDuF?ocdFw z_AP-2B@(vJ2beyacqyd;O9`)+YzW}>r&U$wpN)APR2ONgu95-8d#(UTqiz<*5eHh4 zrp2E|_?W6+fD7|zlD4-~F}3o64OM1kaj~1pS*~tg{{CdS=8NUYIwi|z$ZaBi;*gpv zE-w|Qu5Y@Es)|GK8RF4zM~}H4XtM%8%C3X`3m5L+u!?A>K=S8 z&ApYKe~6cO#z~Vl_L$J!{&eENhKj1!^tS9;xGK!|V(4*|d{gxliv@@PpKAWapQZ4Z zB8thFI_JhMMXRtx0-P!qtq4;>O5B$Utt=mt>7)wElO|YiIbC+7! zYQx8Gb4>Rg0LjXuOW6kp2L=WzLP0$Zkk)pnVXyQEv~{tn6m4yrR`Al zE=s#%P0{yDsPzH_N;y)_&W98#Iy7<$Y03`7L8#j~#cOSa9k5V(Yloll{KB&S@~;Lg zMj!<}COkUa-D>xde`=C$;}2P$K3u_@nl$@En7C;n94tIMLd}F%2eQouQ%LdjA3S{y zq^HOxjtA$2KO|URmNA1@3Dux#`2ck!Lu>J0iX}s9Sy2{W^^m5b#tW9=5B%N~Px!}< z=%yDInl-MU)4ne@y!@05|ND?s{HV6P5`7T5EH;KUC6_~uIL0co1 z_O*TqFsdIBeK^pyu&_A9*rVyrS-obJu}AV;e7!MrMqk~bjuH{JxVZL^qdsd3XMBGF zdg;EFD=IAJ*SS)GKj`pO>5bTH+Q!>28V|3`=G`mGYPv$kGC3dKR`0eTfEX%bGE}v7 z?)8ZtYDqYSn0W{JtFT7Qa_iWufG#xkc?EprMhdjHnFewg13= z+z(xS=gzr(pFaAWo~nsdRhC6VCPD@P0BG`ZQtAKz%z5gvNy_u^I*bb@hKmz4n2 zPLUizKS-JB$eSxE0hplIhya*yD*)&}5$GU-4gdf)9|iymonijFmk<1(Z((lpVgK*- ze}e9IQvm>g7(iZ1T+;*Qv>PE?U)GoHUDryxlewm;YN7IBvvc92?m3Lqdsmw6rgxyK?KcWaq@OblXz?Hf2&*0dvdJ=*T5X`EU-j#c!n1$x!+Ev&bguJ7 z`$MkxIb-kE!Bh_a+<~C;KJQJIlTGV9zTKYIpuQ8kwX@ zj?EuuAyrU}$N-d=&hM0zvxaHbixQ*7w4CJJI!-W}AyqZ1Uofg(FdFoIksuAr5a6?w zK`1*%dHB^GFL%NX4;U#Rm&_!fAKlTFiepHslRBB<#tXcrP;Egk7Ft=*!3!Lxnn8w- z`D#BY&M=X0^9&(G&Dyu-JkZb=a2i{2nLomXCpg1*(q}MNOap8xW_#Pke+g7E<^9 z)iK?Os5}u=s_39g5hh09m3Thh$vO)aYy%f@)+ASq5PIhr1l9qDKa^${WPbMJ%;>Zn zUkEA)H6&Dx2J*K&XI+R6uxqX0HV$eML>|QT&QJCfcbH7TLn#4%lknBK${1nIQyva! zu#6aCM{HOobuc&$Jjn5415C0Eipx`<#{$a?B&l|mNoDr?h%Vn*|F*OZrVr|JkeH$1%nm7o za)yh0;)=}M*nC0gj7udAuiwn=K^}3WOOI!RDZgH}6$v|eHIEAsAE8h>*wW`U3J|x^ z@7@ulWC{b1WLhQq6f^wU@6jD@NZZsuD9Ok}eI!a6in?LciccHFPF*)d^n~a#^O&BI zNVQE4Q(Z^iC-8EwvaM+UE?J=hEfnmC43fNZgw^?m`QQQo0}ze^>a~FS`6_1Mm-Lj@?VDoLeXrLEJgVl8xG)ooDkc|0JxdM4 z?`%3YY}{SX+MquXP?c~mdWo6sV{zMRlLgBR)@3s?0{6tWaZ?>u!Lw`M?#gxfFBkZ5 zLZFhMFXlSZRzsgjP<96zls60i-qowBxC^XfVKl?$+3v-8&_V$_h(k4j!eG4@3z$xl zBoQE*(7X8d5GJob9s*e1P;>dV<^s{n04!RX#6p+U(KhtRupcVrC7~gQGmpC{V_*cP zNCOfJ-hvyb-`_fV3|69M=BY!WqH^I$%U3QehH;TzrK&zZ4Kg z0)gW*MXD&XladSg4O(#~Qj)GULLDi|_t^zFf^m}>Yj?((O|2l7UQU-Eu(gOy#<5*m zrX=Z}{@NJU6^K%(Fzk13dWvm#18ttH#$V@78Gr>)bqeFALsCXXP3=92gYTSPlW_wd zDFE_+tWc%gA;y_GHC_`eB6wx|XdtM!1cK9JkWo1w!an$`}1_Z|}tnuD9fWX+L`Kpjr&@O1g1OO6`RNw@D% z%d?@@Nj=-a;fzf9be$WknBJB4sNxPlM)z1hFWj}X&$ot|!a_l=+-0@8e4ViL znD<`3DJ&uT8v_vNgczOciqbkUOzq8dF#`>tD?UijJ>7pX?6XjA;PSA%j;^HD1Wi!{ zG~d0oiMr`lA!ha_QvDRx7t@)h#rdtmq9 zHdl<}kP%Fdi<9ja__VpYIPfuy2VX_Ud*R~8tPD@ zKo4BT^Fr<1YiWuiaqKv1q{2jiVjL+*RhWSAUOD33s^VbAo}02^DWc`tf*j1>xCNxU ztyotUvX}oHE#yYltlIX~Cv_uDWLr$6WYBljHhYv%1CX@v?$c1gcMVDwCc`~MvKtEV zUreA`TwKiJaA#XDLGxYBbaeKV@FuFbE|Ll8C=1}q6-HN8t?t3lSn z!w*({wkDbq(PcyGAPGr8&aUHnaob*i%s1NwD9TA&a~KB$+y>maBW?Y9b9LjDa%hE%pm)zi=TO{=@s*N%~hQf|#F=b0@ zeNsfs6r1c0zAb&Rz+5l`aoYsx?5)w}B)yH95x7GMT~VkSRi@We+2P|e`LppSA8*tcQQYfKH5wl6RRT_F7wfeQ+V`v4o*(3P3@2uzpGks7HY-v4WmfHl#}d5fqOl|y@KNgfhyGHfCnyO@F&lZh9YP8~q_ zvROn0O}Dxy9Cc%driM#1MH=|sPS?V=ekWiShw%w-@uCk=Vy9SQ2Wv4G)hc`5WH0@e zFjRp7Q_oatXx&`1QC$0Ie7RxGt$yA53o_6`gB=29WWPW2XZb}hZuX~86982d9zfVI zaa}A}aFyP~Jg(b80lKN5@Sqt&asv8a($m#t1uefdWH$B;$n+GWQUq7A`tlR0X2DP? zOkU72`d<@83y5=dmGtfEa@&Fd=(>vQ7Ev=OCv>g8HY|gmu;V`y6=TA5{VQ`UySdku zax1HU{I-jY38f!E4{-!IuDX%mLWEbx=0o{?3y-*g>68I;EC{oNwpv&|6HK*1D_;`4 z;rd#W_M2 zQk1UvdEp7FFt3Q2`y1W4`)~(>=xY9doGdDU9++Okdo0bJdsz;PIm2^{{3%$gI?Y6i z2T5DZT3EB5jN3|@f)&vy)xcNE^fHuh@jhU*p>0?9h7cVnAc=usv7`xax-{vElg%su ztuG8fGQ?Z)GL8>r#xgh9OcNgf*P4Z-{uHy`J?vMN2fzGJ@TshVhpI$P%b{7s;bDY2wj37l z(9l};ug4lcRk(Ka_PtICH)X!@)=%qByb&b|YjPZQr_7lDTmct`(=|IkBS?jvw*)t6 z{(~-|m)w{56Jpu#Z80UR{{@LEXgp4g{FWC2p6B?!G}&q_653O1XUgNi)ypd2rXg_pi)9u1a0!B#}5vA zi*w-uG(aePv)bWCifiB~lQkRH6j3)6mIrkJ%f*}9Nk<2-7?Pt}f;S=aFFbOCz;a)8 z!X!D(`9l{Ev~#g?7y{5j&L9>E_slnj+~Gq>Qa2&6N)67t3OMfJD|x5@>J8yJeO_?++vZYa3L3<3o~`!a`o zrG`Y5ap;5X7!9ZWvi5FN20p(f{^N%vh=(1}~0Mnq8v+1|jID7oLy-rUix1CeVlrtF#^ZjHBJB zwEwXQb#{bRf&EV)z?h0twZt&}|4fQJukHXZj;|Di^Tgn2uQ?>>Z%U9xUSL(Ko+Rm4O&%U&n zPzp6iV0HoioD-~HLhxVt*dB-FvWMqVd5LvkvDD!LGZc-E=J5C-KdmGGMjPc?+Jv$N zO{##vhaO1}{BN#bnOg4ZP=&#jour-hyx(Pz;nn0DoC0W>J~DBaev4>0kcdBNfAKBv zT>AO%M`((kFEemV4A6K-&6m-5^~NWj7=v9>gnqIB8&HxxAh$0;DEEkZB{K zjlX%D6`~vI8+DjB5u6Z0FDphYhKK1I;jz3`ItXZ}(<&wF^pedh!36;^n@t8R&Ci6s zq!+>l6f5r+%s_&{9LA2PVowG#D-@AXZK;QX86yZ=eV_%aQi1fQ{}@#`A6bapy}1a2 z0U^*5<7X8n;?5Od@4{o?jnAh|AY%Xay?ar-!DQebkye@l4^dNGVd0Su_l~!%i4l)h zS&VX4g)t=xd>{>P!u^~2YXxQ3spO1Ds@Otl@r=-Q%ZK3fB~>?q|JW5pfcvYM_q`=? zW{-OEe~q2-S(CYa2qf-&-Zo^Q+LfWSGCQNJ_ANj6p-9^sK1P4~EcNl+^74@)z|Fr2 zD{bb`yxnq%a7i>pK@1=W4l9k!D5{P%vQ(TKomly-dQQiFa}xkNy;No=rqN$jC~gu1 zAo3DESIzy?tj#b?EAO^A%gcxtEyirV2XGMTFDo%QV7*LXu&=yFZflBX+6D_A%8H!@ ze`PCmgpsdF33;Y^|n08Y1K~6D3+&+^KFJ~SAa1?cegR9;6Y>G+Z#uz(P2cLGW2J_mVOU7??P@25 zXdPKwYDNdXFS-*zSmyDSkr(Kb=MnF37~|sx%L_H?ETR!$4dXzIxDKe%t9xNp?xBAY zQx(AkwwRasfb}s|oI-&tBrSERfayurh$;O^!Qr7v&f%51NCYPwu<9jx9$>F@)V!ot zshU%OuS0AI7T}Z-uUNqh`lSpn<@jj*Es*D{+r``LY%HuFLVi&%lo=;R16KKYfAi=w z{_%|WgDo(|i}g}v0>MPAwl@m%6gS=^R(6o5^RsPdXsUlC(!q*=0U8~C`FR-oWb9NI z6z9VNC8qgNe1fvUp@YIa^Qd9TrPuHV4d6OtYu0%ngYOnlR);!<2uJ`%e<3Q?uzgYc zt^4Pui0T1o{rKmUb$?#3G6>%ZxG9I%ia#UM)dd`(bGZ3%D7b?Fk$`HX9Wg&}NjL^+ zroD~Iv-vy#qo&YxcpK-~-gp2(00JY@P2W#UG_oy)OG6b321EuIeX*2nOOlhvlKTXF zY?J;6Z<~73(MY`#8Y_2{T<}tjNb*s6VU=HI85fUP@?k~f)eE-t&Cwqw!^>J(;c-aj zjTQr@>0tuhWBp@V<(O)TwK`!3#_kG%2A|-_wt%qV&zo6P;7w4=pkPB9l|(LusyYZe z7O?30Uv)fU)^SsC5vNw=GdmPvD&R6}ykV8Qr6MH%Q6$CY3|>D_CsH>SBGSW(;0CV4 z#QV9B(8*TtU+YF9tA*tv+}TS?MKx1QY0b%a*?S}M#AQ6wxVR}^QAt3pgdGn?%>MBA zN0dMWi{uv@`b32Jn?~)f(yQS*c@Ic(#eo$$3_q`tzAr)gGx}gSQYW+r(*w=a%|ixN z{nufpNiPZJnBYR8Roj_jB?^rcz)&NYn38c|Nh~f{kOX=D%hc4$tq>d(rU8i-0m3@= z4VlV`Icle=t3n%(=|EHADYP<2UmQvE7L<*Y%%UI314wNTvfjF(D;>FSYzSZ?i(<^} z&%XZm=#ShAV!ze#$7h6%x+!0Vs^@(!)?o(3x@;dALt_DAb$yDSpVGapd-m}^4QCp- zV2bJ11tPj|UOYu8*Z$4IF*_u4ZB-4K5K4Irs3>;L=qut0vD-j!tp{=4$rqGWBQXIf zsR0$5Tar{bqIC}j$uT}mgsB+wl@7f&nM1i=rjxSNjIH=x+0F^W)lOwaSIoc^nSha1 zhfK?AG8^U^y5xe+?FWqX0*T#>Ou@Ar%tO?TNIB`eGOD{F{TCvpOe`fmEJT<=}Ifq`yXeHj z4}Dc*LP9lRCmO1xK#Aw80{{HRu1XNyRxu2YK9ohNRA3%Vg_!z}Zl>XWBrf^axYpg!<9Ug=8>;p^iO1;u#|JK(TdhwLT*uc8qL^PLt zfAle&nGnr{ks=X>E(A5yj95%7vA{1IE4eup&V6p+qQBVfTB3dFor=W$7^5wn6M(ZBbw)k#W?9bSxMfCHBL|K*UQOU7Y8`DQ_2y zNQ*lr$%~*;p5K@<9#knn1oqtw2J-^@6Z~)E#it&qF$l1i$ZNhd_f;4m2~&A3^FOqVSMEx+C@B*PsNW(WHOs*{X45L6mh`kDww|yL=jtaEtH?wKKBS*bTtgFOEME+3;fHlWUwOJ z{w&4g^d`hsFpCN&`47hwnzFlq#U73Z8mt*qOr55DiI>Q}L)*)8|P;sb-m0h{5|_s97g}BrmdG z<@X$BThg1bYUJw#B|Bdz;S2GMe>mG*^<=QLUN%O4xHoo@8zK$#NNJY}fVGBnI=*pf zF-DM#udh~-S74ageT*w4oD+iHi#*{&iAz_04&wc6zSpf9IfP_Z9TZ{Cey=_;!0k)M zXM|>e9tD929oG$@nv=jY%BuCykncF#w^?Tqfo6JMjD@?c;8vfHar3^Y2uqGUKQVB_ z%Nc1XO3ogsjDPWB4J3On?)Qs&H#$vHTQ*B?#^^ir$B+?p>@JB)n~9Mie1@3W zOX~lH@=+QL817RMOGsBhDdgQW*L+OZs>1xR#%OnYx9GYs*$CtC^q|P2eQ_|Y4+FmE zh#J+UvTs>7D1&XNU`al`-QDQJ)(2S{)o`II5o~MTi8dV-XeA{BdRRPBmY3SpbKEX5 zb_LKapuW{CU!i(h7vNXW?6ZiCOywLmb|Zrxb{xEkaW@k*HSpm5l#KSB!ASVu7Y)$F zS|EFoYv&&4$YkM}Auj5o=ZVK3axVKJ=_C1y;Pnf|mG=Zqu3Hoe8Th$tJwauUtOGPo zp~`)M{JziVQ`cpx%haW&m|zqQSw|s(7`XIGSUM}%qWC!hK?Dox{7ep_m1u>X=`#Tgkhc)EHKX11sF>SW^?sp3$7%7hGgnvU=i3S4 zGtas}C4!H%_z2)H5U1$K^cr$cSgyOA<-7na2vS7g-+E;wzbV0I{+T^2VcSz}D0K`5 zr0ct)+=KK(_Zm7O%@;%Nw8e=9hH)rTGe;d!S-#ZxhVj|4S5$;<+uewET-)_cX~Ewf zB>>BJec^_~--ivlVVB07wCWMKjH>lrz^v$52lXQ=!}%np=i&L}H(wm?iV+841ACUa z{(#74{4n4*u10_T(<_C()=qDRW>>0!uTQE0LWE0= zq+rd~BD9#CM%U`P5wn~Q(bX{z_B$CDY=ry~+O4U|84GD}B=q3Gxhr5FF|K(?+-rZL z_ke42#bp+)jmm*TL@G#`$<=J+wjO4$2f(A;&_OJ2{L%kxKi_SL68xgo8bE2>xK1?M zU^pT$J;kU~#>(0<9?~BT^kXHS^;^u}DRvu4OFmJdyRPQ-@AuKh5=XIMsxE9mTXa)zhxoqTaZiD-{(uXOMHi zmukefJfp2V+kmKr{9@JQE7o}lGzqw?uhQF^u2J?NNwcQ;1tKYsVMUk6yGY3C=;Y+o#^ran}olX8u4l;HwOy9BA3ZDAmpo z-oN9U=?PzgEa1M%p)9R!7RJ;A);vS^Ql!8zhOJ<7DIcIjV06XRaZ23wJ(=rj2I)vL zow_ss6P4kM#IzVP&MA?O{hJODzQW4n@Jo7u#(JEg7h(?RS-%~SGD#SQ&k{b6Vi0F- zNCe#VATEgUNns<~Yce{1F?y$tQITxmipQh8pjT~~B*?b~n3OT_{|Uh_Kz<^X+D9YR zMjSOtrd;w?Yq+K`)N9yLANjDW`@o*uP8aadqWeodk+V#*=TI%|b|&qYjbYN9#^3YA zUlYz9c_i@cJ}to*5c`M$M#p%b<~P&PM>Tc90L46=+rbnZ-^)o?_q`*yVD532`JIaIweLCjQ==N0#t!Y5k zOJ;e1pv9Kywj=Qj&PeQ|_ZLEM&ovwwB|%Y?FPoa<_=2l$UWXSAeyCVw2SiTT31V>0 zR32CYT(-aNX-?-7vxN0=w%I*y9lkzI*_e*6ppP~bcEy3U-4YFw_~rUn_81bU+hdp2Da6Lw#8fmVXYkZHvqnv z4v^;IkYpwfMLBS?HcDM!4~#%mg(f;E;G$M#*hMwgErINrDh{gvyq=RuBmvP2;EbQv z*mei|dXb4XxaRtue{~uf*$Q6q*zxrq*(thS^Y4_*{Wm!fn4Gun37fn@d_zkD`vt=c z=MMh>8sbx`M zMo??0Orki!$y_qnR=TyCbo>IL33lq7&i2nTW+TvFCl0E;<-@n1|MQbNCQ`s3kI4{J za;w>*@or7->52_B+c()gjNu9q7_F{yULeDtZo{(3)nn?%Lnf3Tmk-&9b=da3qE8Nf z!%Pd~gX~Xa{YW)mb@YX{Oe~G|xJ$ffq0{03bNba_E9D;8s*!cXd^Pko>@#%U@gW(3 z)XX$c|DxQv>EuSSf)i;puh149uUl%gf&y(}V3h;@{FgkxUR0u05*_qu|BKju4@KHx zBj{EZlumgBI`V|jingymFxZZ!DCV~*S0@{QWWH;mz600 zlD<;reQCG(;LUp4rssr{OTTh%t&9hZ)k*pq=e}t&ME6ApWU$P~;o2%f>mbHNx^s3Y zh_TJAo-!q?b5!JPh`Mk{Ej)FL%{xOV>>Z_d)gYQw$&`=D?%G72il)+|<5WFCxOnJA z%38XdbG;ArGMAB_JpsBLI_D>?8jZQSm)ugQhOexMX+F2iPyCUn*8QAlOzEd0y-}w^ zF*iP&I?`WrV(E--xRP_>_qrEDV;#ri+MGdpCLS@oG5*QP%K_(mU5amSql?8mcraW- zAMOr`-TmV~`RKWL5qf->`6s!FnIJqxTKw7|#o>(SG%1g9?qA8sVR7hFRTnX~L%2x| zK&i7BzportQoF;347iL9*jz=gEEmVE75HmorBB}4-Ec`vwxU{9fYKBF6kSI z$0;q}2FU^|$}N3h3Tu7#%J@Ni&-NMM~i(XXE1S*rRc|?8>** zQdZ$`A^Bug{jp=e@YjBqp>GZ`W_#1SlITONym2aT1^RIu5rdkZ-faw8$%?s z#E(2)Xyj@j)uqM)_1RYKLgbmgxM7)gIXf98pOQw6fp2C+0vx@CZui#~JJND`8-|a4 ztq`K6Yy2ulP+D~`<-x}d_B-l6Df^Lr>Z~;J4WKKU=HFCZRl1UoK-ihMVP=9oNkEy} zuwUOzvena{Nqpxg?_M8%hQ^oPfD9A;vD+jwaedowxfZbnPsp;O7 zgd|6nsJfmfIKQ|)&K6?(My_@`eIoWMbZc?uec`|8_{6=fO9yI=v#X2elg%+pFA?PU z`|(_+DKB1Jl>CUkQlAectJGrNjK!@>ML{EjS#?q5MT)LP<`6w#kjup-J<|M;ATnN3 zl2ytlvu)BLDRnY!zhUPLmWapM$;cNWgU?Wcp34Fl{)CT?CdG+g$l^=#e5b0{qZTC! z#DN%wSf&BE?s{~lb0RW2gg@2kl;TX!I2L?1CKM1bavSU-8OHAuElZ6&sm9lH?Wwy) z*s~gJLX!499AfsZ%LSaceBU}#z+zdOd&w68G-LiK4WPb4EWjlMvCHks+FoybAk26j z=Akw!}89B00WSD|kmVZB;#`5f@dRW{Y1LO?S&7s9!P*+|7cCUZOHa zmpThXLWEdD6^d_vGdZ8emRE_F)B80mGlD)5KiNL2w(25`q2@t( za$KS4CH32k?-UOfS^L@`lgIa5kDpv;b&{vvOZH`7qyHTxvDhi6*1rDben*0)*lqlY z=+5Y%5bI5Lhb@{kI1@z%5PbE%OoO0>OGcYoP}Hf6Dmn6dIU?%s)3z~#W8+Qc^S7@a z3vh5Ij@P^LPs6-^6lE%TslG1`PTYV@*B#{VehQRaDDIs0B5AC$473)sp+iw=1{}KB zK`?MK|N2UcUr2!cjXy1C>{~-T54@qlRy+B55Z&F@atCZmXfSU4>{jc@%1{h5+;>q} z#g_Oy+^|1NwZoy7-823l9yFJoFVGrcFd8!x>uGm?B1kqe!zqN}VXoiv+E{-~HO{`0drr z{qzx?%r-6WZMSw}h56I-M(4epMcMbVCy4{MJntj^cHvzI9{dppLqJd!%yfXYfH!q6&9l!CX*nCE*M<*##}%kw7i8o}Ei z((6KU%V~=h=2=$P9B#QJnG;${Go%5W!<1XhbERj{txnh)kIblHb#=xVJ1I{nP>vTX zX6~dVdhqwxdc0(nSebq=vx_4UVKRK{?;U5%7N&=oOSZaZ%EybmhL9j1CKCG5Z?Ii) zX6UPSX~lB6@($;^g`w-9x*Bpsdvy}(IQmCcNI+y=^ANZ7(_?Q)qw-X`g6?Hb*A$k? zQY)@CA2%*O6QNi_z#Ij(E!UOP{h zdzj~J8V(A7We7~JJ1ly>h6ugRkQ*`Oq-*zv;D2M+7-!&g`G_+{$>1Cl%emiFomW)e zgShYwZiv}@3ns_rjJW`NZqYXoGp0Fuabs-yuo7_fR7FpcjS#Hj5B#lwL6GN6Mg zYg4{HH0rEsTt8?Vb~0BMuXmr|E}sXy(Em52a*-%1bgzkFZbt-5D40T->HqIgW50o> z+pOQxaHlWE)FMqKvto2zFdtn^4R;zp>0Tih(IIctOtn=^AGW$4U#kKDDlLkQWd`?p zp%K7DsLO7WU0PKSDgnRE;{V-?Q)<6dcl73tKL?3Ju;f=h>46gZB>w8WE-Dsda(sfk z*9rPlw?Za7S;ORHtkMN0PYm=iW?vamNeC)EtVuSh79M51EW*3pyWM?Emq$L@2(oc& zkf^wG`bKSd9_Ri6(Q1R*2Ac@lSi*X+jvZ0s48)a;tE-byV4B|Gz?HB%8CPbk7snsi zflz#~_uLe#Kg6Z=zhQjPWDSa?|Mb_*MWV4_7(wTCaAFD}o`{us%QM#AlEM7nmAgNy z==)U82AA_EJ6)KQ7~(VA8+jjM-wT6ZD}Tz74Rh?tlKh35NyjKUH$06hCom%KcU4Ie z5~i77UrFP*8}l2J_p2GX@(n)?yc?v6tQr5v4uHv zZ+we?g7XV-hIeE38T1&n+_4GUDW}HehvNj6R@@~W&o*puNVAiFAwpA=h%Rt5xX03v zxgaPJ`}yC8_@D3#|lznNHRAH(1H(U{|&$rxAm7myFb z^hr=WVF?-2BHtb+cU^8o|EG#rO3ze~9JW-_Y;ER@s4&v7o&AlRDpt`~+-+f0erKFb zyyWkLNgsh05NuN#q?|Fiu8-?oINXymA*XXDD)@febQPqk7gZ!VIxp|Dt^TW%6JxJ( z^BWVxu^p7p#C+v2X#h~;neFba{GXZA9vXSacHyFy8>@64MZ1ZKq96NQV^$-%?ijcT zDB%TsH`>=y0Vbu+1tQ3*f6$~ybI_PY`sUy+6#gjK=_11HNd-R6 zLt5WNdy`c43D+Ej!32CpbI3r+iP4XT1tzg0cG-AT97!cUrKmI5{-}&JDj0?g>424a zEhVyFfBs(1hp=e-YE6ZVm|fu=;3c2lsPxPPe<$_{ zl#Tyd-N4(ym2d=SuyTxg;bTX{^SSol`p9nF^g0u1TSJ`Girr$e=frEqYx?V|B%niW zHrDjjG;eA-i!Vmlb+uX2u|*l3Cts1*aYwC!9w>SBNLL;mILigTh-Y`kFkZC+)axLB zz8Cv7SQLN@VoX7TxvY7w`Pn3Kb;lAFl)&{N#l;h-pP(Ndq25kcRJ@+9@uB~>8wXX~ z5W{oIFkQR%$>TyN8v#a?NFNg-qsFs*(FDG)HyqJx)#B^eHE zcfNKYis>dbe@zaO0W2>H{S+?wra7->>?4f_COk|$9b&R{PoXz8CUU~c7FtrH8Jw2i z(9LJ4i2axoLhxLWP_#SL7raMfzT0kTvL@1g^Dr$>?t^c=8KU+F!J=~`B2W5ltoI!? zPg;0mvMW|Jj6KTDz#Q_gka;>O^}FPRiP1B~AivKwpC=jdgMp;T3hZhLhxze3K)lrM z)acgC?HR%~78gGKIqP7V--bTPf`#UMPc{Qc)xU(wgCCX1a}Kl*=3l5H00$JEVqv1G zNWC0|ySdoOL(R%y`$ z(UD_>!%q_)wt`{Q+JC>gae8zdRg+S9Xb7_xCja2Cr%n0~>+ zER{+=J7bI2tLe17RXhC6!u6qV;QAk#zxW4b5*8DR{+3AMzqv1vn~e!%9T%L*d*@ck zwC*$I8zCPX-|A`a<+HZeOj*kF{Zu;7zgGv|aNnOvY8{Rt6OA`3Ah%V)ytikFoyYml zGQQlH+!)fF$V5S)Z{UqwIjzG!`lWKYum_!sRjGgxc$S@GhbOXYW(RcYvTtBo1s2qK zF=xN+^d^hC!qfe;x%G-t*JH#ug+s)BrHAUP?#S_o5z5W?axu2|ihwb6U@sf<^Gur4 zIjIkVS>yjG*o%AoNw_ETCG}Ux)IhU#Nq0?0m%GjIkB6!23;&MYU(S2Mve{We5912PyVm1n9Tk7Rylo(bh2do)3vTE z^=tgr!ZHK(J>)P`GW!R%jIYbI&upJ*gd2^>+{t4Z`Li9-H%XiUN57lEUm60AJwhA>*NUerrLzt>kaffKSu=rMrOu0xo(R!F=R? zBtH6Zy8#y4^dF$!PDTtV%WPN6VrGCj`%>(lJjM~KG>bn&d}|c{1^nFeLVAPKDhKgo zN{Uknjk@2zY)a^W-I=`?k1+v~w?ft6lQlyMDmf5f{ul$zz1(6#+>DYY0@4bh!jI~+uL@ct7G&PvtIL)%NdCY9%DHizt zXh9r<*8qC!zl;~K31Qkt--ug&2R?gQ+1b0B=?GJuc>(+g3pY7^BC6Z+4~zu9e(8bB(D4W)WtYn@jQf3i`(8#qK8T%yLmd` zThOb+%_tc|x6DE%iIHRv7v_&>oAad032Mh|)Vsi`q98pu1>wLt%?-nC*ZN4~!!$Eu zxpcF?0wW6CV0Hwo&nF$VX;eK@u5PP- zm9;3tliSJGiG51W3-}%e&x4-)B`qgOR4hP*kAnYSAznt-`KP+A?8ud==CLe!&ZWjP z^DHGDK?4&Dmooo@#w*zz)ST=w2YU1sr>Ah|%t+%BN7dg2GUBA-!l&9@0aEsVSv_le zm(Q95kS^m=GTqiVpm3?M@A zKsTQb>6lsjjofR4=XU)>H47W!63UhQ=lY<(1M5E2&jj!j99}0X(oBk;9@Np$z(yuEkcMv_s^!8Na-GbNOz|mtR zi?96PX5c%{bdyGzp&P>#d%gNN6}Sd*F2AiulH&sg9;OJbJ&4a>S1XA_H7VbNlx1m~ zKligx!Nak)Aq?*bZCF8GUU#m810s(QVXPI|AEfRri6D)i=TVsBI-1ziyu22*jx5{` zq;^~XPSNl??fh<{s$e?@{d@1xJdf{Ku3v}GuA4i_5?t5#ug3nU>9MSZkzV?;=i48S zvNWHZo=!xppZNOiO%`JnZw9dS)O78V90hN@*L|N`w@NdEt%J8X<#**UFM1g(nyb6X zaBN#yadhsGSL-@5TR}NMF$q`6$hd^|A8=X%t2h_He9K&$b4Xv;v$#bP#Dg;-BuxG0 z0JC#zM}xC3ECo@5*7;UfOM+-_Ccr$ECt*Fo*_8b2>QY@>4M>|Uhh>u0kp70rlHM-y z1>Aa(JhEhWeVy1K8mfEzsTnmTm=8tNb;Ru`cts$C49Run$NtUl=^m;By~U_Ud^89-_pZD?-A}Q*Jt>jU+1=Ro3%KH>U0I9_x!wn> z3N1}-TqQ5viWC79X(A#^B~yErnc70Vp8H>(HiJ)JUa^KQ&iE9ToFb}+Nx=hWvb*cZTrL`!`vsFu z_a)3`hHC~Z&%;_KIkR7%G%Yc@iGmuxFHg7da<^2Y{?C5_95h8UEQM~iEgOp^KUDTPVqsV!Zbkz zt%Y>};vMBbPFRFbw@%XM^akAXH{15v3=oo%f^Fp)qf?rv`wd;f# z8}>2(B+EYShzN6Bp2&W3Pt3|W8#y}nF@L2g>z<1{A7}+<%muIxTkcQ z#0BjsbvYk#X6~ZC+IMy6&nz-v5}_=hN=M34NnMryvu3}|{VRYw6^q?B!-iuuc+UMV zdk_uDzBAGx^X!Zy3WXM9NjyaZsOBV21fH=`&KvhR>2hD%uL-vG|hhuoIqRC`S&Go0slAs&k9>`HWTk0`yuYyk(( z$oSU-mNDD(wbm?OO_lK>)treG${e@$+_N_4ryCr$x1SkVUj?OaJS^^h5gBbugS)V- zKjGFT)rL-So#A>g?8z7bvgrYA=zI_2tyxazKe1e--(QS5F2UC1_;|F&Xry{}s{Gcd zV%G#Dyx=dH>EV`+L>N;0r)%_+%OUrdmslAWIo=*g)4ewe8A64Zif?|mmvaVc`^^WA z9sCzvTJP6RU1S`E1&`S4^gXYP?I*ImiJkuUHvD_b?b9vOQ&l9Ue?R^2yn7m(`#Z-j zIZ4$qWPGy~y;9FiY~Ha9dMw8fV!(0IOXn#+`MHVFPQ-_3U#G545;$oFcP=<7FP~nZ ztsjAc#J+kEAj;lvYm7+L&nZO^>KC>pWRF0XdF%`g@%g1_xfbOIe`OxxjtJQGl zsm6IptbX19vHW$Ke1jziJ#2q(Yw~@27JUb+y-kf=4%d7lk8*BbDNUO)NYpFjlwHvd zZGLj`W{{qWEG{0iVj>8U5d(PU;?syf)+vvEO`Gv*dP>bL?1NA}3 z+VD(W$sK`O;4~J@ft$)=yS9!pR6XG1_*$S$&>he6+I|V^CK#0jIxxZ8Q3qg_BIi-S zXFT#TqP!>uAA4VyV^=1X!8jg9rd}xB_uN`JX00U`8lL{wZQCtJI^rv}oHlow{LQyo zW$`(SyNi#PJtI@Jb@uS~bGPR%dp;X?<7S?z8!~qGvu!r+b{H>rrymoR{<+T1kBskv z;=)4igNE>+#C7&-p>6>bmqPKZwvr11z=-^Tle6(I&atq9D4qKQP?X$Mc(0l}zR+D! zQ1@wIX*uBtxUlWorKOiEG#g^m7fKvRG zWr5`+6A{-W8LyN18awKSIoftkGmC2MY%}eg&&ICHsXLrkcw=vLXE^grokPNdP~6^2 zog3+tJP{znBC~oI)Q6xr5%3=E!7-&Y9sT>bn3g^7KB`zVbWEWKLzY5uN+5(zrV(mp zAdv+V$r3~p+d7)IbB8q5xeGdegO8~cO8skt+2r5ckoUbPG;0;}KWx-6`P8kSlDGWX zbyAI^ZiWoQ%%?m3^Vu23>Bn|A{=G!}ZhnoO3G4Q3yOFU|rb5O}aXe@645Q?U04F4G zf}%K{dMH<7Bb`_f+S5F=A5NCyMtM7~*fI;&1UPxn4Rs!(jS40lsGP-e#*sLi8dP5^ zKmOP|Wx>SJy^3)A@1Bzfo?q9?K9|L;v88V%Xu|sakbwhoB@;V0ZkW5`#w##e{~MVy zXIeKy_9{pr%+TpK*R$fqoS}23{$Z$h zLGiP$oXMw}006I$U4o82u*qN;fTB<3@=8vMRe|i1d;udYR7j8veHbcvnw>_4&2y1{ z{I+9vfkqMO(yK3&v+>c9YnRQHu|t}0*Ls^AIKufAW>}EB|K^!BLNWU6aU*0Pn14Q+ zf={geHl&o&$XZg$4a=(ZomOT}FnN1-@)WJea;&J1}Efw(kj`V<~l<`#uC zuVS#nq4;noR4fRC*XsLFZ-18~x3PT(F2hkZlq)|M$WeHQ6SocL6=^ual=JawL|(4T za>|+SOx(M1z~<&#-YeX|#S2_!j2R|3ox4z`jA)Uz1MRY7|DlY(HQU*wTm`JhP8ut#u2_{hufB2J#_n_L zHd7?V_OoHWPJ7#rsTV(XwjVog6Nk}lf6la>ew=>k&bY~y$k;i2p?Hm*nwwu(D`oq* z$pB(G6q!5)9_>(Rq$aeN&1gd>;!t?xwF(UV(CQrdjU_|>Iv6?DK}H2w3BwTA)Bxiv zDJnFuQRt8^Dk+i*2IiXQ(8Y7)Ld?)@B4eM5S@v}+=EV6F195T=SYndSmSdx#QpNl(jFc=`Ozg^4n&5u%B&n zJ~uz^#!s$zjUVbxvcuVCS9;_K31_ zAagJDSS&vvue(1bvdya=VmCu?#l`^a<)yei5TY0ZjhJiIkr>P)Fa6QD$j~E^pJG5h ziuLlF{`#G`3C>>;TChoNY+a4K^!;DRkbQe)!13eK7k9$*^QY}tFW<8FpgjA=HhB^= z^Flw$!u;3bd+XnN|8+74YxQZRrXhpzz4etce#$u6^!g@gKX^vUiuTq`w%J&sO=if= z&G@04@n`Iux-&fen=no{JFJ_V)6V&9;$|<+89T-C^uxttr(akpWf9{OoQW+nSde~pVutRS*Vr-iD{+Pp6%g2f8x5CJHa6m@&~lV7GUES2 zLE&2FqH8X~m&^PgpC@zq%4feK2lgG1YFzv>0B6{Tw71L1{rhF)f&J2S_=q$dJ0=aV zufvtHeZb^T_U$XDA;Ql`Lk9MjufO*#vTW+Od|p%GK4Q!WIq%Bzq#pYf*1z()bmD_L zBsU~&JDTn1OrWkmr+qQc&M+o^V{de)y?LhY^kdtR!IP=8ePWiQUwi`wIVe?E%%W}7 zqwTzr1CPi!VL*EN-t0$uT=NtwhW-^?ZwT;t#xgKMd>kMIxm&)Jyzxc44iQjN7)xpc zlYPf6@5yY2^HTsa^IJc2t8Cr4)zy0+?1r!FJRt)+j!EOuBhq~Muna$VP{v>;Kjd(G zhPfZddhtnzG@u6I-nMeSd=<=`8}XFRAVA5<Sm@WEqKd!WoV1leXF$Ww!e+QPU+Cqfhy7tNlP`bz%d%y|mW(~>q9Cfl^|DPz zj>xF}`!X~7{t(p<29xK^etcV-41wDPm!B(rF`HNFjQ~FtyYTFVat<&WJz+EkVFU14 z*(y2Gend{4EUa(yTX%l_*}1jD*k;HVKo0AGj$;NOfvz zZpy5$bB&#J8ngAiZF^(~uIJspd7Er~V=Kh->t)C0?bvW~I`hd>!~6`^)X5;JX!d%i zjDL9P;l~bR{J6zo`(qqVvcuYDr$76ddFBqVb|`M3ql!((h`feOtQKA|%gG0s;2)u$ zh04BsFvO8i7D|74Us2@gV3rS_aLZ^crY_&cXOq!d#-jjk@3>pJeA^+y3Gf_+^XE;- z%ay(}9x;!6n@_Y1mUmz?9M3}Mqt(A&EvxT(C{v;IZ9>@Qd~xJ*H}P!7?EjK$FTu5_ z+C}HW%XVY^@%{i_D?17I#7~_*Ne1AOntCjc^oQc*2!o&ocIh-zH9-xAqB})iTsPvh zj&+fX(VM!5w+uO-4P!R$)SZ5u&wl(cUS~M=l3Zt}&MlqnAf0|-rIba0)l3QJVqkd^ zIBkd7t-7+lqy5k0HOznH32pMoJswBbV$sWCL^1{_``a>IV0EAoa+iA)h@;-v5*OAk zzUq8mJKH%s|IO`pcbC=RZ+DzD+xEuX9Vmyvj;9mn+4=48S|2yj<%lwsPRDVp-^1Vg zxx9Gqqe2}05}6^sA9om!Ib=OD{h4B1F1-^ZE^< zVn&T0B~3U+r&JgMyCG)(ufP8%*}G*&VF5jSvo4J|o5xDXEGgoq;_Tf?_PgM02gZ%M zakI^g%lYgUzX@yHOuV)|8NSIUnL3$!BZ?rG5eq2^5;%(`kV&=GVz~&@{4i|n-`Ik2pBtz0=O{uu{rO zRQck)3M}sfM(iRYO_0R&VZFzJV$0C+!i>S_ol>uI68FQfZ>boeAx%T0kIMkD2Tla< z7~lS=eQ|9rXID43j?=c|%h=YcbqWxQ*AYUz%^G3;>v`TzP_ zS(&+=GV74DUMkMod5Z~$5vD$!bquFoM8C!#b<+-vo&9XgjKucKXOdf(tV{Nv;h9gf z#%`nzv3z;$f|bY2jdRn0+0(%1LFg2AM3TTHeeC>+M%H7=&_5?K1mXklFLeZjEgD=D zZbF(7P;o+yhMgO?g)TaKkqpO$C4T65$8l-HSJz4*)9L1lu`>?o~%T%z@5#aa7MD9+mXQgX1!Hb!#RIqhvb<1gd6%gxvsfBBvn*69bGXLmX# z9^;Qo1sr@(akft8&N3+VK!A5^oCRFx!S(^D-}i8h$O8r(9j5Lhk|obeyj-flj*$9TD2texlX^C zKZGxT+055AGYj)BZN)a@F+;|V4}RK{9giKx84qXcHZhztEJg9WfsP#tH_>s{9y#uXJXMeuqhse1 z=TD(H_K5_cu8-PnB@&tP$OPIGo0PNZWqe1Dp?^_iIG6>QT$r>CiA@bNK`X7Y?clf4 z2;7*tfuXvuTyyg^1;xE*7Z14QC%ZGv=X+4oAbIP1u9LGbKHJwB_(~oU$+M029J{$kkjUU_2Y0qaee;&`HQgJ*$=t&cS@n68| zhrrWchtM;8Ii4ajxbRcBRxouajvaeYq!{{_FiXbEJCw7(FbU~xLAuJ&eS%K$B6lTU zdhNxyd}O%KP+4z#cTp&LhU|{j?kwrUJllRuXKP~MedKR@|DVVJe6dZbcm$sQ`+YWXyMYS->hyVyt*h}Rb28cM=oTAInZ$}1EtK*Q#(AzqYIf$2!_YzaO7b+oLe*7R-#7zhM z9OqSUe{;e4cFxe<$_`9C8-ejp2L7l2<7Qd3a&f_V&;UIVID%!6HIF>e(=x(OhOzTT zI*Q`CX2v#Sh;77vHulCYU;A>pJD(Y!iOaM{+HqEH(l+*XSWZ;9Y+!Gqi`0w?c?+4i zB5^Y?`W5hcI^?wN546TJre zrAAx!@a()=p!m}6gCb)mv5k<}j%;(rvg^;-8#~kHXzv!z_HX<$tg$zC=9z9@c4Yi~ zhp4zKo?MN(@egB~h~Fc>76opB&Rgbe2t&aE>Fx_X(Gp9B!C`b6`fb_U;5gu}7@<

^htaU*Zpi;V%+zFc)w_6!t}0P^hGOe#J@ z|2p=JV0~#+5oHQA2r86fm#t`2;{uI1L6K1=)gPkvoBrw@MVD%yV}|bR`0Qsp5sLWV zgYT@*&>ieHEG3)QJ^GX!@^KAs$q9uJ0mXa6ptxSmofY;%6FewDkOC^0)ugs?6LCoD%K+ct@yxXNkG>7o5AwM?#H&q9~ihw&iSJ z&gYCr%dhQj!W-TArS4_t^ke)Qd#`Zl6EjN)Tt=Xo{!&C|gHz27tGr0-1O`q$cj|_f zcnRr5Qsn+symJ7QlBP%yMiqPT^6)%jXn8`KhvM+Ua@4Wx@^fX@;@L&ll`U=u#=VF* z7<-z>Oc;&hXxGb}C853Fz4DY~iw#e{C_7)fCAWp42ul1lBzKgzS2KQ(*B$IU+JbGD_2N)9iAT>`XZB51`^qjI(w!c;g4gB++1$>jo6)2T7Q1_N8t=;JBdB`AkgiO1RLqcch-@?a~@PZABDuO`i6kZxK z*-(zrA-#5_$3Zs-+mR#QO?~clmrGw<^I9q$In0x{*_K$_zg|{du}Ut)ZF7TL_6am-oqmJj)+p<)z%|oWfDZV|b%Y??;A6@_Np?p83z^pKi{0&V=mkX5ykBhI2mK z(@?rQ{W!zXona_Jqv8fY&f3jJKp2xi=b#YVpk55RFj&gi7DXv>Ddh~6p?@0}$3W;h zs&v*+N|AkqC4&ux@KR$nQ(nW%S^RFZGUoIoZe&|=>B>M-IC1>AG!7mFCVq*W_onlu zegL~4r7#g_$Jg#2|LL#8OtPYB&ADI&HtL*~#~=88CLd&iBquYqpJ_J!jBebW&$hjB zbH>kSL&lGt2HniFtsAomWBW5^>LmK%RFjIGx|y}xVPPFtx)$Z&=Cz$c6#(&_1|Ql^ zjN?cR9+?i6p<@&|8X!iVpQDmH!h8{s)*94LBpLcVh}!$r1~cYA2fO6o_JNzEh9AdO zst5wB|Mhk`j-!KVp&93$C08z)hh?2*@}qBjU-mgJHp+*CVXDTT^V!(D>4sVO?MsY* zXI#!SozL`d+ff|PWd_dLDfZ*AIr5z@@|c7Ap?q(J`e!oY9R7SeL9($2afLQ<^PRC0 zJ%%Q6_&h}IXlRiYwL5SOyQ0@2{XiP3qZ}nYp1?-66%dODMH8k?RAwF&m_@|1@tK$9 z)!+WHh>-CN(hpY?kMFvOX!coi60>I%!qC#ZZkPAM(Olp7t(g4y&Z?` zX6%if^Vu1vaX0q%voYJcowl(zb`-@QgxU+mnLC+%+z4g55P2?{XLZf;xh|hLDj6I< zh(O*S3sJi%3mQAyf7rA(>;&_rH~WHR`MjumOb~eN2mdMixAU%!^w5;E7rGX93I?kG z^P7JwSG?^?d=sE=4+d-k7=}pQkeAuGJ9XpV#AUl1H`|V8;EH+R~R zk&~fw*3L606wk+qh9a-?OU*BEY6Ih|L?2&j*7JrhJ462pzL@|38}sHQLLQJ`?kNeC zkmgHB5Lz^-s!t!8Q{)F&qEQ0Pg+XB3E3eCw|L;EYm5x6RX+2z z&&s$d<9jet=Syfm)9go?blt+*ZpPh?)41EZ)7^eHX8Jc~>V}Njb|YiwPKy0qDn{b? z3y|fKwgjN;+Yv{s340?3VwSt!WQG_O#q_``xl_YhW8`dnQt$rxV9I|8cg2wl7{&i#LTnNX>a@*v)8i;6 z^7%95P1upYbom0Q4QPkIZ{P#=xccAjkbMaor1*ho>dM7_yk-3GQ@oemaD^;5dy)L$ zYyTnZUdXO~;a%+RHEFhqy=g9Yl{{a5&D71Dniier(22<7B9g0S~U6_m$PJeQ5x-j+Qr!SCC~ zkt#AZ#f!`D@;-$|x3torO-rD-{pgXSvL52+wdk;4e(ELJjH^#iV0Qj0z-EU=;`1ZV zz9Nr84e4Je7od~B2uGe~fyoDPxlT5faQ@f*?rC}MzgLHoScFpHa?#Wk3kuHB@X^EN z<6roMJc94T|NOf@mt$xzB*Rdt+r&0w?2Os8Bl>f?*

D{!Lh?e`9ZSJB;nu*xPo_ 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

H(SP z$ji^U3!4*S@26rN7Zz^`^!GfR_;?3{McK2%+dO4tG+$M+!ZIckhiAOxb+_!5BT1v? z+uD+|qfkP3J6~xGvIlW_yUN{slKF4{D3r(6L^aCJ`<&XbE=CfQVs*1y8{F%2)3L2~t1TmGN0m-)>5p4i@1~$2N!;LxlHb209=z$k2LKV0&zNN{0_GBW+NllayfI}7VjNVT6&mpSO=Z1)<=Jn z5(*C)Yx_KM^9sM!6rJR`eZR+VIXq$_A~+(W-E8V|l{GZoeUy_VVOyhab#4Dh1Zu11mQ%G%suT!}?(t21Y%>sRjh70jb*oSad<#pvlB z{@nBuN%OALnxGPihg8gd&7zHm6ZzEOcaH{$TQiXNae%L}Yb=vd*QeSG z-WOFUrRUpCeXgG0C>>dtI;zqz8u>!)JFNq*OpIBuvrF!wp3|xHIH0zh`ndY#KxcN5 zBgycVb-_v*&+(&P`vUdKxTFeMD_4i0v3;m7b{C zpakL+ajnC-qOW0~V{j`!?cC$N=wZKTYJ^GO77`Sb9i(@$oTKWYpkhQwNa%c4J0?NT zb=J=Ke|hbv(XLzuR}U(i8~9!OV@LY7Eeg-55JXjHbSR_TkjpH%woh8xFOjrtx&HRk ziMQ;oysaL)JV4cegO_#Iliy5yH!sJFii$=LwfTE4xpUFEU*051;p5_VxKHguAuEMU z$zPKtn5NKy;382pAXh;VpiINT#e13BXSuB>(!E%jD*-O+ii|veghkVBtHBPBI@K=d zfv2OXC$QwshLQVY5D;B*)t%qlc{9FcrhlSx_68GESM{>d75`kB1fLY(% zp$PB4b$r~O2~71;P&1MLTQ{Nq*)$2kmH&Sg11f;)gMF9ZeE)ao7}h8NG}+&@&i>yr z3*)4~H5iQN_Ts-1X_$dmEPwn*+cIigQADkuhddVLzp@X|9^`W9%Rz?hm{{0JLI?a+ zr2ly+XmDt#xTy)m-t`fLNwXr|om(!H$S7!E zOn-a(+;ymzo4AQ&+FebKs&*Gq-7VhVu=tKEv9)U{koT1LV5r7U+sLHj*uB+gHbrlnUuEcWdfM!B@kL6UGw<_*nUaOes _insertBusStops(Map stops) async { stops.forEach((stopCode, stopData) async { await insertInDatabase('favoritestops', - {'stopCode': stopCode, 'favorited': stopData.favorited ? '1' : '0'}); + {'stopCode': stopCode, 'favorited': stopData.favorited.toString()}); for (var busCode in stopData.configuredBuses) { await insertInDatabase( 'busstops', diff --git a/uni/lib/controller/local_storage/app_courses_database.dart b/uni/lib/controller/local_storage/app_courses_database.dart index f85e35b44..650cc5640 100644 --- a/uni/lib/controller/local_storage/app_courses_database.dart +++ b/uni/lib/controller/local_storage/app_courses_database.dart @@ -71,6 +71,5 @@ class AppCoursesDatabase extends AppDatabase { final batch = db.batch(); batch.execute('DROP TABLE IF EXISTS courses'); batch.execute(createScript); - await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_lectures_database.dart b/uni/lib/controller/local_storage/app_lectures_database.dart index d4ec97b53..079d7a21c 100644 --- a/uni/lib/controller/local_storage/app_lectures_database.dart +++ b/uni/lib/controller/local_storage/app_lectures_database.dart @@ -10,7 +10,7 @@ import 'package:sqflite/sqflite.dart'; class AppLecturesDatabase extends AppDatabase { static const createScript = '''CREATE TABLE lectures(subject TEXT, typeClass TEXT, - startDateTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; + day INTEGER, startTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; AppLecturesDatabase() : super( @@ -19,7 +19,7 @@ class AppLecturesDatabase extends AppDatabase { createScript, ], onUpgrade: migrate, - version: 6); + version: 5); /// Replaces all of the data in this database with [lecs]. saveNewLectures(List lecs) async { @@ -33,10 +33,11 @@ class AppLecturesDatabase extends AppDatabase { final List> maps = await db.query('lectures'); return List.generate(maps.length, (i) { - return Lecture.fromApi( + return Lecture.fromHtml( maps[i]['subject'], maps[i]['typeClass'], - maps[i]['startDateTime'], + maps[i]['day'], + maps[i]['startTime'], maps[i]['blocks'], maps[i]['room'], maps[i]['teacher'], @@ -76,6 +77,5 @@ class AppLecturesDatabase extends AppDatabase { final batch = db.batch(); batch.execute('DROP TABLE IF EXISTS lectures'); batch.execute(createScript); - await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/app_shared_preferences.dart index 9134e9ef4..63aec56ec 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/app_shared_preferences.dart @@ -7,6 +7,7 @@ import 'package:tuple/tuple.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/utils/favorite_widget_type.dart'; + /// Manages the app's Shared Preferences. /// /// This database stores the user's student number, password and favorite @@ -150,18 +151,18 @@ class AppSharedPreferences { .toList(); } + static saveHiddenExams(List newHiddenExams) async { final prefs = await SharedPreferences.getInstance(); - prefs.setStringList(hiddenExams, newHiddenExams); + prefs.setStringList( + hiddenExams, newHiddenExams); } static Future> getHiddenExams() async { final prefs = await SharedPreferences.getInstance(); - final List storedHiddenExam = - prefs.getStringList(hiddenExams) ?? []; + final List storedHiddenExam = prefs.getStringList(hiddenExams) ?? []; return storedHiddenExam; } - /// Replaces the user's exam filter settings with [newFilteredExamTypes]. static saveFilteredExams(Map newFilteredExamTypes) async { final prefs = await SharedPreferences.getInstance(); diff --git a/uni/lib/controller/local_storage/notification_timeout_storage.dart b/uni/lib/controller/local_storage/notification_timeout_storage.dart index 4f9173d8e..6a36bc427 100644 --- a/uni/lib/controller/local_storage/notification_timeout_storage.dart +++ b/uni/lib/controller/local_storage/notification_timeout_storage.dart @@ -36,8 +36,8 @@ class NotificationTimeoutStorage{ return DateTime.parse(_fileContent[uniqueID]); } - Future addLastTimeNotificationExecuted(String uniqueID, DateTime lastRan) async{ - _fileContent[uniqueID] = lastRan.toIso8601String(); + void addLastTimeNotificationExecuted(String uniqueID, DateTime lastRan) async{ + _fileContent.putIfAbsent(uniqueID, () => lastRan.toString()); await _writeToFile(await _getTimeoutFile()); } diff --git a/uni/lib/controller/logout.dart b/uni/lib/controller/logout.dart index 65e4d3965..e6b71ad13 100644 --- a/uni/lib/controller/logout.dart +++ b/uni/lib/controller/logout.dart @@ -16,6 +16,7 @@ import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; + Future logout(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); final faculties = await AppSharedPreferences.getUserFaculties(); diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index 9899be172..c3dfec1fa 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -178,13 +178,13 @@ class NetworkRouter { /// Makes an HTTP request to terminate the session in Sigarra. static Future killAuthentication(List faculties) async { - final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; - final response = await http - .get(url.toUri()) - .timeout(const Duration(seconds: loginRequestTimeout)); + final url = + '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; + final response = await http. + get(url.toUri()).timeout(const Duration(seconds: loginRequestTimeout)); if (response.statusCode == 200) { Logger().i("Logout Successful"); - } else { + }else{ Logger().i("Logout Failed"); } return response; diff --git a/uni/lib/controller/parsers/parser_calendar.dart b/uni/lib/controller/parsers/parser_calendar.dart index 497da6c41..27dc81110 100644 --- a/uni/lib/controller/parsers/parser_calendar.dart +++ b/uni/lib/controller/parsers/parser_calendar.dart @@ -7,10 +7,10 @@ import 'package:uni/model/entities/calendar_event.dart'; Future> getCalendarFromHtml(Response response) async { final document = parse(response.body); - final List calendarHtml = document.querySelectorAll('tr'); + final List calendarHtml = + document.querySelectorAll('tr'); - return calendarHtml - .map((event) => CalendarEvent( - event.children[0].innerHtml, event.children[1].innerHtml)) - .toList(); + return calendarHtml.map((event) => + CalendarEvent(event.children[0].innerHtml, event.children[1].innerHtml) + ).toList(); } diff --git a/uni/lib/controller/parsers/parser_exams.dart b/uni/lib/controller/parsers/parser_exams.dart index aa7661f5e..74491601c 100644 --- a/uni/lib/controller/parsers/parser_exams.dart +++ b/uni/lib/controller/parsers/parser_exams.dart @@ -46,8 +46,8 @@ class ParserExams { exams.querySelectorAll('td.exame').forEach((Element examsDay) { if (examsDay.querySelector('a') != null) { subject = examsDay.querySelector('a')!.text; - id = Uri.parse(examsDay.querySelector('a')!.attributes['href']!) - .queryParameters['p_exa_id']!; + id = Uri.parse(examsDay.querySelector('a')!.attributes['href']!).queryParameters['p_exa_id']!; + } if (examsDay.querySelector('span.exame-sala') != null) { rooms = @@ -60,8 +60,8 @@ class ParserExams { DateTime.parse('${dates[days]} ${splittedSchedule[0]}'); final DateTime end = DateTime.parse('${dates[days]} ${splittedSchedule[1]}'); - final Exam exam = Exam(id, begin, end, subject ?? '', rooms, - examTypes[tableNum], course.faculty!); + final Exam exam = + Exam(id,begin, end, subject ?? '', rooms, examTypes[tableNum],course.faculty!); examsList.add(exam); }); @@ -73,4 +73,5 @@ class ParserExams { }); return examsList; } + } diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 4d83e9bb9..5517042ca 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/lecture.dart'; -import 'package:uni/model/entities/time_utilities.dart'; Future> parseScheduleMultipleRequests(responses) async { List lectures = []; @@ -21,7 +20,6 @@ Future> parseSchedule(http.Response response) async { final json = jsonDecode(response.body); - final schedule = json['horario']; for (var lecture in schedule) { @@ -36,16 +34,12 @@ Future> parseSchedule(http.Response response) async { final String classNumber = lecture['turma_sigla']; final int occurrId = lecture['ocorrencia_id']; - final DateTime monday = DateTime.now().getClosestMonday(); - - final Lecture lec = Lecture.fromApi(subject, typeClass, monday.add(Duration(days:day, seconds: secBegin)), blocks, - room, teacher, classNumber, occurrId); - - lectures.add(lec); - + lectures.add(Lecture.fromApi(subject, typeClass, day, secBegin, blocks, + room, teacher, classNumber, occurrId)); } final lecturesList = lectures.toList(); + lecturesList.sort((a, b) => a.compare(b)); if (lecturesList.isEmpty) { diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index 428bbf98b..788a6689c 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:http/http.dart' as http; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart'; @@ -7,16 +8,11 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/entities/time_utilities.dart'; - - Future> getOverlappedClasses( Session session, Document document) async { final List lecturesList = []; - final DateTime monday = DateTime.now().getClosestMonday(); - final overlappingClasses = document.querySelectorAll('.dados > tbody > .d'); for (final element in overlappingClasses) { final String? subject = element.querySelector('acronym > a')?.text; @@ -38,12 +34,7 @@ Future> getOverlappedClasses( final String? classNumber = element.querySelector('td[headers=t6] > a')?.text; - try { - final DateTime fullStartTime = monday.add(Duration( - days: day, - hours: int.parse(startTime!.substring(0, 2)), - minutes: int.parse(startTime.substring(3, 5)))); final String? link = element.querySelector('td[headers=t6] > a')?.attributes['href']; @@ -54,14 +45,14 @@ Future> getOverlappedClasses( await NetworkRouter.getWithCookies(link, {}, session); final classLectures = await getScheduleFromHtml(response, session); - lecturesList.add(classLectures .where((element) => element.subject == subject && - element.startTime == fullStartTime) + startTime?.replaceFirst(':', 'h') == element.startTime && + element.day == day) .first); } catch (e) { - final Lecture lect = Lecture.fromHtml(subject!, typeClass!, monday.add(Duration(days: day)), + final Lecture lect = Lecture.fromHtml(subject!, typeClass!, day, startTime!, 0, room!, teacher!, classNumber!, -1); lecturesList.add(lect); } @@ -79,10 +70,6 @@ Future> getScheduleFromHtml( var semana = [0, 0, 0, 0, 0, 0]; final List lecturesList = []; - - final DateTime monday = DateTime.now().getClosestMonday(); - - document.querySelectorAll('.horario > tbody > tr').forEach((Element element) { if (element.getElementsByClassName('horas').isNotEmpty) { var day = 0; @@ -120,7 +107,7 @@ Future> getScheduleFromHtml( final Lecture lect = Lecture.fromHtml( subject, typeClass, - monday.add(Duration(days: day)), + day, startTime, blocks, room ?? '', diff --git a/uni/lib/model/entities/bug_report.dart b/uni/lib/model/entities/bug_report.dart index 9596c7eeb..3ef4b22d7 100644 --- a/uni/lib/model/entities/bug_report.dart +++ b/uni/lib/model/entities/bug_report.dart @@ -1,18 +1,24 @@ /// Stores information about Bug Report import 'package:tuple/tuple.dart'; -class BugReport { +class BugReport{ final String title; final String text; final String email; - final Tuple2? bugLabel; + final Tuple2? bugLabel; final List faculties; - BugReport(this.title, this.text, this.email, this.bugLabel, this.faculties); - Map toMap() => { - 'title': title, - 'text': text, - 'email': email, - 'bugLabel': bugLabel!.item2, - 'faculties': faculties - }; -} + BugReport( + this.title, + this.text, + this.email, + this.bugLabel, + this.faculties + ); + Map toMap() => { + 'title':title, + 'text':text, + 'email':email, + 'bugLabel':bugLabel!.item2, + 'faculties':faculties + }; +} \ No newline at end of file diff --git a/uni/lib/model/entities/calendar_event.dart b/uni/lib/model/entities/calendar_event.dart index eebe459cd..cf6e94ae8 100644 --- a/uni/lib/model/entities/calendar_event.dart +++ b/uni/lib/model/entities/calendar_event.dart @@ -8,6 +8,9 @@ class CalendarEvent { /// Converts the event into a map Map toMap() { - return {'name': name, 'date': date}; + return { + 'name': name, + 'date': date + }; } } diff --git a/uni/lib/model/entities/exam.dart b/uni/lib/model/entities/exam.dart index eec51bce7..9dce8c199 100644 --- a/uni/lib/model/entities/exam.dart +++ b/uni/lib/model/entities/exam.dart @@ -58,12 +58,12 @@ class Exam { 'Exames ao abrigo de estatutos especiais': 'EAE' }; - Exam(this.id, this.begin, this.end, this.subject, this.rooms, this.type, - this.faculty); + Exam(this.id, this.begin, this.end, this.subject, this.rooms, this.type, this.faculty); static List displayedTypes = types.keys.toList().sublist(0, 4); - Exam.secConstructor(this.id, this.subject, this.begin, this.end, String rooms, - this.type, this.faculty) { + + Exam.secConstructor( + this.id, this.subject, this.begin, this.end, String rooms, this.type,this.faculty) { this.rooms = rooms.split(','); } @@ -76,7 +76,7 @@ class Exam { 'end': DateFormat("yyyy-MM-dd HH:mm:ss").format(end), 'rooms': rooms.join(','), 'examType': type, - 'faculty': faculty + 'faculty':faculty }; } diff --git a/uni/lib/model/entities/lecture.dart b/uni/lib/model/entities/lecture.dart index 166c72d80..a22a60274 100644 --- a/uni/lib/model/entities/lecture.dart +++ b/uni/lib/model/entities/lecture.dart @@ -1,56 +1,76 @@ import 'package:logger/logger.dart'; +import 'package:uni/model/entities/time_utilities.dart'; /// Stores information about a lecture. class Lecture { String subject; + String startTime; + String endTime; String typeClass; String room; String teacher; String classNumber; - DateTime startTime; - DateTime endTime; + int day; int blocks; + int startTimeSeconds; int occurrId; /// Creates an instance of the class [Lecture]. Lecture( this.subject, this.typeClass, - this.startTime, - this.endTime, + this.day, this.blocks, this.room, this.teacher, this.classNumber, - this.occurrId); + int startTimeHours, + int startTimeMinutes, + int endTimeHours, + int endTimeMinutes, + this.occurrId) + : startTime = '${startTimeHours.toString().padLeft(2, '0')}h' + '${startTimeMinutes.toString().padLeft(2, '0')}', + endTime = '${endTimeHours.toString().padLeft(2, '0')}h' + '${endTimeMinutes.toString().padLeft(2, '0')}', + startTimeSeconds = 0; factory Lecture.fromApi( String subject, String typeClass, - DateTime startTime, + int day, + int startTimeSeconds, int blocks, String room, String teacher, String classNumber, int occurrId) { - final endTime = startTime.add(Duration(seconds:60 * 30 * blocks)); + final startTimeHours = (startTimeSeconds ~/ 3600); + final startTimeMinutes = ((startTimeSeconds % 3600) ~/ 60); + final endTimeSeconds = 60 * 30 * blocks + startTimeSeconds; + final endTimeHours = (endTimeSeconds ~/ 3600); + final endTimeMinutes = ((endTimeSeconds % 3600) ~/ 60); final lecture = Lecture( subject, typeClass, - startTime, - endTime, + day, blocks, room, teacher, classNumber, + startTimeHours, + startTimeMinutes, + endTimeHours, + endTimeMinutes, occurrId); + lecture.startTimeSeconds = startTimeSeconds; return lecture; } factory Lecture.fromHtml( String subject, String typeClass, - DateTime day, + int day, String startTime, int blocks, String room, @@ -65,12 +85,15 @@ class Lecture { return Lecture( subject, typeClass, - day.add(Duration(hours: startTimeHours, minutes: startTimeMinutes)), - day.add(Duration(hours: startTimeMinutes+endTimeHours, minutes: startTimeMinutes+endTimeMinutes)), + day, blocks, room, teacher, classNumber, + startTimeHours, + startTimeMinutes, + endTimeHours, + endTimeMinutes, occurrId); } @@ -79,7 +102,8 @@ class Lecture { return Lecture.fromApi( lec.subject, lec.typeClass, - lec.startTime, + lec.day, + lec.startTimeSeconds, lec.blocks, lec.room, lec.teacher, @@ -89,7 +113,8 @@ class Lecture { /// Clones a lecture from the html. static Lecture cloneHtml(Lecture lec) { - return Lecture.clone(lec); + return Lecture.fromHtml(lec.subject, lec.typeClass, lec.day, lec.startTime, + lec.blocks, lec.room, lec.teacher, lec.classNumber, lec.occurrId); } /// Converts this lecture to a map. @@ -97,7 +122,8 @@ class Lecture { return { 'subject': subject, 'typeClass': typeClass, - 'startDateTime': startTime.toIso8601String(), + 'day': day, + 'startTime': startTime, 'blocks': blocks, 'room': room, 'teacher': teacher, @@ -108,22 +134,23 @@ class Lecture { /// Prints the data in this lecture to the [Logger] with an INFO level. printLecture() { - Logger().i(toString()); - } - - @override - String toString() { - return "$subject $typeClass\n$startTime $endTime $blocks blocos\n $room $teacher\n"; + Logger().i('$subject $typeClass'); + Logger().i('${TimeString.getWeekdaysStrings()[day]} $startTime $endTime $blocks blocos'); + Logger().i('$room $teacher\n'); } /// Compares the date and time of two lectures. int compare(Lecture other) { - return startTime.compareTo(other.startTime); + if (day == other.day) { + return startTime.compareTo(other.startTime); + } else { + return day.compareTo(other.day); + } } @override int get hashCode => Object.hash(subject, startTime, endTime, typeClass, room, - teacher, startTime, blocks, occurrId); + teacher, day, blocks, startTimeSeconds, occurrId); @override bool operator ==(other) => @@ -134,6 +161,8 @@ class Lecture { typeClass == other.typeClass && room == other.room && teacher == other.teacher && + day == other.day && blocks == other.blocks && + startTimeSeconds == other.startTimeSeconds && occurrId == other.occurrId; } diff --git a/uni/lib/model/entities/time_utilities.dart b/uni/lib/model/entities/time_utilities.dart index d1d512d5a..9180392b8 100644 --- a/uni/lib/model/entities/time_utilities.dart +++ b/uni/lib/model/entities/time_utilities.dart @@ -1,12 +1,9 @@ -import 'package:flutter/material.dart'; - extension TimeString on DateTime { String toTimeHourMinString() { return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; } - static List getWeekdaysStrings( - {bool startMonday = true, bool includeWeekend = true}) { + static List getWeekdaysStrings({bool startMonday = true, bool includeWeekend = true}) { final List weekdays = [ 'Segunda-Feira', 'Terça-Feira', @@ -25,13 +22,3 @@ extension TimeString on DateTime { return includeWeekend ? weekdays : weekdays.sublist(0, 5); } } - -extension ClosestMonday on DateTime{ - DateTime getClosestMonday(){ - final DateTime day = DateUtils.dateOnly(this); - if(day.weekday >=1 && day.weekday <= 5){ - return day.subtract(Duration(days: day.weekday-1)); - } - return day.add(Duration(days: DateTime.daysPerWeek - day.weekday+1)); - } -} \ No newline at end of file diff --git a/uni/lib/utils/duration_string_formatter.dart b/uni/lib/utils/duration_string_formatter.dart deleted file mode 100644 index 91eef0fa7..000000000 --- a/uni/lib/utils/duration_string_formatter.dart +++ /dev/null @@ -1,46 +0,0 @@ -extension DurationStringFormatter on Duration{ - - static final formattingRegExp = RegExp('{}'); - - String toFormattedString(String singularPhrase, String pluralPhrase, {String term = "{}"}){ - if (!singularPhrase.contains(term) || !pluralPhrase.contains(term)) { - throw ArgumentError("singularPhrase or plurarPhrase don't have a string that can be formatted..."); - } - if(inSeconds == 1){ - return singularPhrase.replaceAll(formattingRegExp, "$inSeconds segundo"); - } - if(inSeconds < 60){ - return pluralPhrase.replaceAll(formattingRegExp, "$inSeconds segundos"); - } - if(inMinutes == 1){ - return singularPhrase.replaceAll(formattingRegExp, "$inMinutes minuto"); - } - if(inMinutes < 60){ - return pluralPhrase.replaceAll(formattingRegExp, "$inMinutes minutos"); - } - if(inHours == 1){ - return singularPhrase.replaceAll(formattingRegExp, "$inHours hora"); - } - if(inHours < 24){ - return pluralPhrase.replaceAll(formattingRegExp, "$inHours horas"); - } - if(inDays == 1){ - return singularPhrase.replaceAll(formattingRegExp, "$inDays dia"); - } - if(inDays <= 7){ - return pluralPhrase.replaceAll(formattingRegExp, "$inDays dias"); - - } - if((inDays / 7).floor() == 1){ - return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semana"); - } - if((inDays / 7).floor() > 1){ - return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semanas"); - } - if((inDays / 30).floor() == 1){ - return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} mês"); - } - return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} meses"); - - } -} \ No newline at end of file diff --git a/uni/lib/view/about/about.dart b/uni/lib/view/about/about.dart index 411a1901d..1bf311d1d 100644 --- a/uni/lib/view/about/about.dart +++ b/uni/lib/view/about/about.dart @@ -19,8 +19,7 @@ class AboutPageViewState extends GeneralPageViewState { children: [ SvgPicture.asset( 'assets/images/ni_logo.svg', - colorFilter: - ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), + color: Theme.of(context).primaryColor, width: queryData.size.height / 7, height: queryData.size.height / 7, ), diff --git a/uni/lib/view/bug_report/widgets/form.dart b/uni/lib/view/bug_report/widgets/form.dart index f63d5b9fb..c4c21e8da 100644 --- a/uni/lib/view/bug_report/widgets/form.dart +++ b/uni/lib/view/bug_report/widgets/form.dart @@ -61,7 +61,6 @@ class BugReportFormState extends State { bugDescriptions.forEach((int key, Tuple2 tup) => {bugList.add(DropdownMenuItem(value: key, child: Text(tup.item1)))}); } - @override Widget build(BuildContext context) { return Form( @@ -140,7 +139,7 @@ class BugReportFormState extends State { child: Text( '''Encontraste algum bug na aplicação?\nTens alguma ''' '''sugestão para a app?\nConta-nos para que possamos melhorar!''', - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.center), ), ); @@ -156,7 +155,7 @@ class BugReportFormState extends State { children: [ Text( 'Tipo de ocorrência', - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.left, ), Row(children: [ @@ -192,7 +191,7 @@ class BugReportFormState extends State { child: CheckboxListTile( title: Text( '''Consinto que esta informação seja revista pelo NIAEFEUP, podendo ser eliminada a meu pedido.''', - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.left), value: _isConsentGiven, onChanged: (bool? newValue) { @@ -234,15 +233,14 @@ class BugReportFormState extends State { setState(() { _isButtonTapped = true; }); - final List faculties = - await AppSharedPreferences.getUserFaculties(); + final List faculties = await AppSharedPreferences.getUserFaculties(); final bugReport = BugReport( - titleController.text, - descriptionController.text, - emailController.text, - bugDescriptions[_selectedBug], - faculties) - .toMap(); + titleController.text, + descriptionController.text, + emailController.text, + bugDescriptions[_selectedBug], + faculties + ).toMap(); String toastMsg; bool status; try { @@ -264,17 +262,13 @@ class BugReportFormState extends State { if (mounted) { FocusScope.of(context).requestFocus(FocusNode()); - status - ? ToastMessage.success(context, toastMsg) - : ToastMessage.error(context, toastMsg); + status ? ToastMessage.success(context, toastMsg) : ToastMessage.error(context, toastMsg); setState(() { _isButtonTapped = false; }); } } - - Future submitGitHubIssue( - SentryId sentryEvent, Map bugReport) async { + Future submitGitHubIssue(SentryId sentryEvent, Map bugReport) async { final String description = '${bugReport['bugLabel']}\nFurther information on: $_sentryLink$sentryEvent'; final Map data = { @@ -282,7 +276,7 @@ class BugReportFormState extends State { 'body': description, 'labels': ['In-app bug report', bugReport['bugLabel']], }; - for (String faculty in bugReport['faculties']) { + for (String faculty in bugReport['faculties']){ data['labels'].add(faculty); } return http @@ -297,7 +291,7 @@ class BugReportFormState extends State { }); } - Future submitSentryEvent(Map bugReport) async { + Future submitSentryEvent(Map bugReport) async { final String description = bugReport['email'] == '' ? '${bugReport['text']} from ${bugReport['faculty']}' : '${bugReport['text']} from ${bugReport['faculty']}\nContact: ${bugReport['email']}'; diff --git a/uni/lib/view/bug_report/widgets/text_field.dart b/uni/lib/view/bug_report/widgets/text_field.dart index ae021e20c..6504609fb 100644 --- a/uni/lib/view/bug_report/widgets/text_field.dart +++ b/uni/lib/view/bug_report/widgets/text_field.dart @@ -35,7 +35,7 @@ class FormTextField extends StatelessWidget { children: [ Text( description, - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.left, ), Row(children: [ @@ -52,9 +52,9 @@ class FormTextField extends StatelessWidget { decoration: InputDecoration( focusedBorder: const UnderlineInputBorder(), hintText: hintText, - hintStyle: Theme.of(context).textTheme.bodyMedium, + hintStyle: Theme.of(context).textTheme.bodyText2, labelText: labelText, - labelStyle: Theme.of(context).textTheme.bodyMedium, + labelStyle: Theme.of(context).textTheme.bodyText2, ), controller: controller, validator: (value) { diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index e13275642..fd6da89de 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -92,7 +92,7 @@ class NextArrivalsState extends State { ); result.add( Container( - padding: EdgeInsets.only(top: 15), + padding: const EdgeInsets.only(top: 15), child: ElevatedButton( onPressed: () => Navigator.push( context, @@ -134,7 +134,7 @@ class NextArrivalsState extends State { child: Text('Não foi possível obter informação', maxLines: 2, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium))); + style: Theme.of(context).textTheme.subtitle1))); return result; } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart index f0712c2c9..ef2f81889 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart @@ -49,7 +49,7 @@ class BusStopRow extends StatelessWidget { return Text('Não há viagens planeadas de momento.', maxLines: 3, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium); + style: Theme.of(context).textTheme.subtitle1); } Widget stopCodeRotatedContainer(context) { @@ -57,7 +57,7 @@ class BusStopRow extends StatelessWidget { padding: const EdgeInsets.only(left: 4.0), child: RotatedBox( quarterTurns: 3, - child: Text(stopCode, style: Theme.of(context).textTheme.titleMedium), + child: Text(stopCode, style: Theme.of(context).textTheme.subtitle1), ), ); } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index c1ec2d134..9221f0cb9 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart @@ -28,7 +28,6 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { num = estimatedTime.minute; final String minute = (num >= 10 ? '$num' : '0$num'); - return Text('$hour:$minute', - style: Theme.of(context).textTheme.titleMedium); + return Text('$hour:$minute', style: Theme.of(context).textTheme.subtitle1); } } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart index 391146f72..3c56378be 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart @@ -21,14 +21,14 @@ class TripRow extends StatelessWidget { Text(trip.line, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium), + style: Theme.of(context).textTheme.subtitle1), Text(trip.destination, - style: Theme.of(context).textTheme.titleMedium), + style: Theme.of(context).textTheme.subtitle1), ], ), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${trip.timeRemaining}\'', - style: Theme.of(context).textTheme.titleMedium), + style: Theme.of(context).textTheme.subtitle1), EstimatedArrivalTimeStamp( timeRemaining: trip.timeRemaining.toString()), ]) diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart index 7402800a7..88b58ae68 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart @@ -84,7 +84,7 @@ class BusStopSearch extends SearchDelegate { updateStopCallback); return AlertDialog( title: Text('Seleciona os autocarros dos quais queres informação:', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), content: SizedBox( height: 200.0, width: 100.0, @@ -93,7 +93,7 @@ class BusStopSearch extends SearchDelegate { actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), + style: Theme.of(context).textTheme.bodyText2), onPressed: () => Navigator.pop(context)), ElevatedButton( child: const Text('Confirmar'), diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 4942cc005..57d1a78fd 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -52,13 +52,13 @@ class CalendarPageViewState extends GeneralPageViewState { child: Text(calendar[index].name, style: Theme.of(context) .textTheme - .titleLarge + .headline6 ?.copyWith(fontWeight: FontWeight.w500)), ), oppositeContentsBuilder: (context, index) => Padding( padding: const EdgeInsets.all(24.0), child: Text(calendar[index].date, - style: Theme.of(context).textTheme.titleMedium?.copyWith( + style: Theme.of(context).textTheme.subtitle1?.copyWith( fontStyle: FontStyle.italic, )), ), diff --git a/uni/lib/view/common_widgets/date_rectangle.dart b/uni/lib/view/common_widgets/date_rectangle.dart index b2e1a7d3d..153d4b18c 100644 --- a/uni/lib/view/common_widgets/date_rectangle.dart +++ b/uni/lib/view/common_widgets/date_rectangle.dart @@ -15,7 +15,7 @@ class DateRectangle extends StatelessWidget { margin: const EdgeInsets.only(bottom: 10), alignment: Alignment.center, width: double.infinity, - child: Text(date, style: Theme.of(context).textTheme.titleSmall), + child: Text(date, style: Theme.of(context).textTheme.subtitle2), ); } } diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index dc92d9d04..be81dd354 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -37,7 +37,7 @@ abstract class GenericCard extends StatefulWidget { Text getInfoText(String text, BuildContext context) { return Text(text, textAlign: TextAlign.end, - style: Theme.of(context).textTheme.titleLarge!); + style: Theme.of(context).textTheme.headline6!); } showLastRefreshedTime(String? time, context) { @@ -53,7 +53,7 @@ abstract class GenericCard extends StatefulWidget { return Container( alignment: Alignment.center, child: Text('última atualização às ${parsedTime.toTimeHourMinString()}', - style: Theme.of(context).textTheme.bodySmall)); + style: Theme.of(context).textTheme.caption)); } } @@ -105,12 +105,10 @@ class GenericCardState extends State { margin: const EdgeInsets.only(top: 15, bottom: 10), child: Text(widget.getTitle(), style: (widget.smallTitle - ? Theme.of(context) - .textTheme - .titleLarge! + ? Theme.of(context).textTheme.headline6! : Theme.of(context) .textTheme - .headlineSmall!) + .headline5!) .copyWith( color: Theme.of(context).primaryColor)), )), diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index f7429d9dd..9d88e714c 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -29,7 +29,7 @@ class GenericExpansionCardState extends State { title: Text(widget.getTitle(), style: Theme.of(context) .textTheme - .headlineSmall + .headline5 ?.apply(color: Theme.of(context).primaryColor)), elevation: 0, children: [ diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 4b5138ce2..23617e16e 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -53,7 +53,7 @@ class _LastUpdateTimeStampState extends State { children: [ Text( 'Atualizado há $elapsedTimeMinutes minuto${elapsedTimeMinutes != 1 ? 's' : ''}', - style: Theme.of(context).textTheme.titleSmall) + style: Theme.of(context).textTheme.subtitle2) ]); } } diff --git a/uni/lib/view/common_widgets/page_title.dart b/uni/lib/view/common_widgets/page_title.dart index 7cebe9f0a..9977d4fd6 100644 --- a/uni/lib/view/common_widgets/page_title.dart +++ b/uni/lib/view/common_widgets/page_title.dart @@ -14,8 +14,8 @@ class PageTitle extends StatelessWidget { Widget build(BuildContext context) { final Widget title = Text( name, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).primaryTextTheme.headlineMedium?.color), + style: Theme.of(context).textTheme.headline4?.copyWith( + color: Theme.of(context).primaryTextTheme.headline4?.color), ); return Container( padding: pad ? const EdgeInsets.fromLTRB(20, 20, 20, 10) : null, 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 ad8f9f20e..152923cb6 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -107,8 +107,7 @@ abstract class GeneralPageViewState extends State { } }, child: SvgPicture.asset( - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), + color: Theme.of(context).primaryColor, 'assets/images/logo_dark.svg', height: queryData.size.height / 25, ), diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart index cacfd953c..ac5e65da2 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart @@ -77,7 +77,7 @@ class AppNavigationDrawerState extends State { child: Text(logOutText, style: Theme.of(context) .textTheme - .titleLarge! + .headline6! .copyWith(color: Theme.of(context).primaryColor)), ), ); diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index f5f0b2d1d..8ba72d406 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -50,15 +50,13 @@ class RequestDependentWidgetBuilder extends StatelessWidget { ? contentGenerator(content, context) : onNullContent; } - if (contentLoadingWidget != null) { + if (contentLoadingWidget != null){ return contentChecker ? contentGenerator(content, context) - : Center( - child: Shimmer.fromColors( - baseColor: Theme.of(context).highlightColor, - highlightColor: - Theme.of(context).colorScheme.onPrimary, - child: contentLoadingWidget!)); + : Center(child: Shimmer.fromColors( + baseColor: Theme.of(context).highlightColor, + highlightColor: Theme.of(context).colorScheme.onPrimary, + child: contentLoadingWidget!)); } return contentChecker ? contentGenerator(content, context) @@ -82,7 +80,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { return Center( heightFactor: 3, child: Text('Sem ligação à internet', - style: Theme.of(context).textTheme.titleMedium)); + style: Theme.of(context).textTheme.subtitle1)); } } return Column(children: [ @@ -90,7 +88,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { padding: const EdgeInsets.only(top: 15, bottom: 10), child: Center( child: Text('Aconteceu um erro ao carregar os dados', - style: Theme.of(context).textTheme.titleMedium))), + style: Theme.of(context).textTheme.subtitle1))), OutlinedButton( onPressed: () => Navigator.pushNamed(context, '/${DrawerItem.navBugReport.title}'), diff --git a/uni/lib/view/common_widgets/toast_message.dart b/uni/lib/view/common_widgets/toast_message.dart index 65854de38..0a1170e47 100644 --- a/uni/lib/view/common_widgets/toast_message.dart +++ b/uni/lib/view/common_widgets/toast_message.dart @@ -75,9 +75,9 @@ class ToastMessage { barrierDismissible: false, barrierColor: Colors.white.withOpacity(0), context: context, - builder: (toastContext) { + builder: (_) { Future.delayed(const Duration(milliseconds: 2000), () { - Navigator.of(toastContext).pop(); + Navigator.of(context).pop(); }); return mToast; }); diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 19a2b94e4..d45b605ce 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -87,7 +87,7 @@ class CourseUnitsPageViewState onNullContent: Center( heightFactor: 10, child: Text('Não existem cadeiras para apresentar', - style: Theme.of(context).textTheme.titleLarge), + style: Theme.of(context).textTheme.headline6), )) ]); } @@ -142,7 +142,7 @@ class CourseUnitsPageViewState return Center( heightFactor: 10, child: Text('Sem cadeiras no período selecionado', - style: Theme.of(context).textTheme.titleLarge)); + style: Theme.of(context).textTheme.headline6)); } return Expanded( child: Container( diff --git a/uni/lib/view/exams/widgets/day_title.dart b/uni/lib/view/exams/widgets/day_title.dart index 0dc07515c..fc11f0236 100644 --- a/uni/lib/view/exams/widgets/day_title.dart +++ b/uni/lib/view/exams/widgets/day_title.dart @@ -18,7 +18,7 @@ class DayTitle extends StatelessWidget { alignment: Alignment.center, child: Text( '$weekDay, $day de $month', - style: Theme.of(context).textTheme.titleLarge, + style: Theme.of(context).textTheme.headline6, ), ); } diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 766092afa..53db40292 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -19,11 +19,11 @@ class ExamFilterFormState extends State { Widget build(BuildContext context) { return AlertDialog( title: Text('Definições Filtro de Exames', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), actions: [ TextButton( child: - Text('Cancelar', style: Theme.of(context).textTheme.bodyMedium), + Text('Cancelar', style: Theme.of(context).textTheme.bodyText2), onPressed: () => Navigator.pop(context)), ElevatedButton( child: const Text('Confirmar'), @@ -43,7 +43,8 @@ class ExamFilterFormState extends State { Widget getExamCheckboxes( Map filteredExams, BuildContext context) { - filteredExams.removeWhere((key, value) => !Exam.types.containsKey(key)); + filteredExams + .removeWhere((key, value) => !Exam.types.containsKey(key)); return ListView( children: List.generate(filteredExams.length, (i) { final String key = filteredExams.keys.elementAt(i); diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index 21fbc1347..fadfe586c 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -30,8 +30,7 @@ class ExamRow extends StatefulWidget { class _ExamRowState extends State { @override Widget build(BuildContext context) { - final isHidden = - Provider.of(context).hiddenExams.contains(widget.exam.id); + final isHidden = Provider.of(context).hiddenExams.contains(widget.exam.id); final roomsKey = '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.beginTime}-${widget.exam.endTime}'; return Center( @@ -53,8 +52,8 @@ class _ExamRowState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ ExamTime( - begin: widget.exam.beginTime, - ) + begin: widget.exam.beginTime, + end: widget.exam.endTime) ]), ExamTitle( subject: widget.exam.subject, @@ -106,7 +105,7 @@ class _ExamRowState extends State { List roomsList(BuildContext context, List rooms) { return rooms .map((room) => - Text(room.trim(), style: Theme.of(context).textTheme.bodyMedium)) + Text(room.trim(), style: Theme.of(context).textTheme.bodyText2)) .toList(); } diff --git a/uni/lib/view/exams/widgets/exam_time.dart b/uni/lib/view/exams/widgets/exam_time.dart index 1c0615690..443441e84 100644 --- a/uni/lib/view/exams/widgets/exam_time.dart +++ b/uni/lib/view/exams/widgets/exam_time.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; class ExamTime extends StatelessWidget { final String begin; + final String end; - const ExamTime({Key? key, required this.begin}) + const ExamTime({Key? key, required this.begin, required this.end}) : super(key: key); @override @@ -12,7 +13,8 @@ class ExamTime extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max, children: [ - Text(begin, style: Theme.of(context).textTheme.bodyMedium), + Text(begin, style: Theme.of(context).textTheme.bodyText2), + Text(end, style: Theme.of(context).textTheme.bodyText2), ], ); } diff --git a/uni/lib/view/exams/widgets/exam_title.dart b/uni/lib/view/exams/widgets/exam_title.dart index 743a0a952..8fb7c91eb 100644 --- a/uni/lib/view/exams/widgets/exam_title.dart +++ b/uni/lib/view/exams/widgets/exam_title.dart @@ -20,12 +20,9 @@ class ExamTitle extends StatelessWidget { Widget createTopRectangle(context) { final Text typeWidget = Text(type != null ? ' ($type) ' : '', - style: Theme.of(context).textTheme.bodyMedium); - final Text subjectWidget = Text(subject, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.apply(color: Theme.of(context).colorScheme.tertiary)); + style: Theme.of(context).textTheme.bodyText2); + final Text subjectWidget = + Text(subject, style: Theme.of(context).textTheme.headline5?.apply(color: Theme.of(context).colorScheme.tertiary)); return Row( children: (reverseOrder diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index ff567736a..685d31101 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -50,7 +50,7 @@ Widget getCardContent(BuildContext context, Map stopData, b Text('Configura os teus autocarros', maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall!.apply()), + style: Theme.of(context).textTheme.subtitle2!.apply()), IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.push( @@ -77,7 +77,7 @@ Widget getCardContent(BuildContext context, Map stopData, b Container( padding: const EdgeInsets.all(8.0), child: Text('Não foi possível obter informação', - style: Theme.of(context).textTheme.titleMedium)) + style: Theme.of(context).textTheme.subtitle1)) ]); } } @@ -88,7 +88,7 @@ Widget getCardTitle(context) { children: [ const Icon(Icons.directions_bus), // color lightgrey Text('STCP - Próximas Viagens', - style: Theme.of(context).textTheme.titleMedium), + style: Theme.of(context).textTheme.subtitle1), ], ); } diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 331a48e26..aa7d4f268 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -46,7 +46,7 @@ class ExamCard extends GenericCard { contentChecker: exams.isNotEmpty, onNullContent: Center( child: Text('Não existem exames para apresentar', - style: Theme.of(context).textTheme.titleLarge), + style: Theme.of(context).textTheme.headline6), ), contentLoadingWidget: const ExamCardShimmer().build(context), ); @@ -106,7 +106,7 @@ class ExamCard extends GenericCard { return Container( margin: const EdgeInsets.only(top: 8), child: RowContainer( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).backgroundColor, child: Container( padding: const EdgeInsets.all(11), child: Row( @@ -116,7 +116,7 @@ class ExamCard extends GenericCard { children: [ Text( '${exam.begin.day} de ${exam.month}', - style: Theme.of(context).textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyText1, ), ExamTitle( subject: exam.subject, type: exam.type, reverseOrder: true) diff --git a/uni/lib/view/home/widgets/exam_card_shimmer.dart b/uni/lib/view/home/widgets/exam_card_shimmer.dart index 55cb29ee3..8f85f59e4 100644 --- a/uni/lib/view/home/widgets/exam_card_shimmer.dart +++ b/uni/lib/view/home/widgets/exam_card_shimmer.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -class ExamCardShimmer extends StatelessWidget { - const ExamCardShimmer({Key? key}) : super(key: key); - +class ExamCardShimmer extends StatelessWidget{ + const ExamCardShimmer({Key? key}): super(key: key); + @override Widget build(BuildContext context) { return Center( - child: Container( + child: Container( padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), margin: const EdgeInsets.only(top: 8.0), child: Column( @@ -24,80 +24,63 @@ class ExamCardShimmer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ - //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - height: 2.5, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox(height: 2.5,), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + + ], + ) ]), - Container( - height: 30, - width: 100, - color: Colors.black, - ), //UC section - Container( - height: 40, - width: 40, - color: Colors.black, - ), //Calender add section + Container(height: 30, width: 100, color: Colors.black,), //UC section + Container(height: 40, width: 40, color: Colors.black,), //Calender add section ], )), - const SizedBox( - height: 10, - ), - Row( - //Exam room section - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) + const SizedBox(height: 10,), + Row( //Exam room section + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox(width: 10,), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox(width: 10,), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox(width: 10,), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + ], + ) ], ))); } + + + } diff --git a/uni/lib/view/home/widgets/exit_app_dialog.dart b/uni/lib/view/home/widgets/exit_app_dialog.dart index d078f05ac..057575fd1 100644 --- a/uni/lib/view/home/widgets/exit_app_dialog.dart +++ b/uni/lib/view/home/widgets/exit_app_dialog.dart @@ -17,7 +17,7 @@ class BackButtonExitWrapper extends StatelessWidget { context: context, builder: (context) => AlertDialog( title: Text('Tens a certeza de que pretendes sair?', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(false), diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 857a55140..ee2fa9c63 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -85,7 +85,7 @@ class MainCardsList extends StatelessWidget { return AlertDialog( title: Text( 'Escolhe um widget para adicionares à tua área pessoal:', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), content: SizedBox( height: 200.0, width: 100.0, @@ -94,7 +94,7 @@ class MainCardsList extends StatelessWidget { actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), + style: Theme.of(context).textTheme.bodyText2), onPressed: () => Navigator.pop(context)) ]); }), //Add FAB functionality here @@ -148,7 +148,7 @@ class MainCardsList extends StatelessWidget { .setHomePageEditingMode(!editingModeProvider.isEditing), child: Text( editingModeProvider.isEditing ? 'Concluir Edição' : 'Editar', - style: Theme.of(context).textTheme.bodySmall)) + style: Theme.of(context).textTheme.caption)) ]), ); } diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 4eeb035d6..69255adfa 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -32,7 +32,7 @@ class RestaurantCard extends GenericCard { contentChecker: restaurantProvider.restaurants.isNotEmpty, onNullContent: Center( child: Text('Não existem cantinas para apresentar', - style: Theme.of(context).textTheme.headlineMedium, + style: Theme.of(context).textTheme.headline4, textAlign: TextAlign.center)))); } diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index a17fdc2d0..2ba31b108 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -10,6 +10,7 @@ import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import 'package:uni/view/home/widgets/schedule_card_shimmer.dart'; import 'package:uni/utils/drawer_items.dart'; + class ScheduleCard extends GenericCard { ScheduleCard({Key? key}) : super(key: key); @@ -32,7 +33,7 @@ class ScheduleCard extends GenericCard { contentChecker: lectureProvider.lectures.isNotEmpty, onNullContent: Center( child: Text('Não existem aulas para apresentar', - style: Theme.of(context).textTheme.titleLarge, + style: Theme.of(context).textTheme.headline6, textAlign: TextAlign.center)), contentLoadingWidget: const ScheduleCardShimmer().build(context)) ); @@ -48,27 +49,41 @@ class ScheduleCard extends GenericCard { } List getScheduleRows(BuildContext context, List lectures) { + if (lectures.length >= 2) { + // In order to display lectures of the next week + final Lecture lecturefirstCycle = Lecture.cloneHtml(lectures[0]); + lecturefirstCycle.day += 7; + final Lecture lecturesecondCycle = Lecture.cloneHtml(lectures[1]); + lecturesecondCycle.day += 7; + lectures.add(lecturefirstCycle); + lectures.add(lecturesecondCycle); + } final List rows = []; final now = DateTime.now(); var added = 0; // Lectures added to widget - DateTime lastAddedLectureDate = DateTime.now(); // Day of last added lecture + var lastDayAdded = 0; // Day of last added lecture + final stringTimeNow = (now.weekday - 1).toString().padLeft(2, '0') + + now.toTimeHourMinString(); // String with current time within the week for (int 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: TimeString.getWeekdaysStrings()[(lectures[i].startTime.weekday-1) % 7])); + final stringEndTimeLecture = lectures[i].day.toString().padLeft(2, '0') + + lectures[i].endTime; // String with end time of lecture + + if (stringTimeNow.compareTo(stringEndTimeLecture) < 0) { + if (now.weekday - 1 != lectures[i].day && + lastDayAdded < lectures[i].day) { + rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[i].day % 7])); } rows.add(createRowFromLecture(context, lectures[i])); - lastAddedLectureDate = lectures[i].startTime; + lastDayAdded = lectures[i].day; added++; } } if (rows.isEmpty) { - rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[0].startTime.weekday % 7])); + rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[0].day % 7])); rows.add(createRowFromLecture(context, lectures[0])); } return rows; diff --git a/uni/lib/view/home/widgets/schedule_card_shimmer.dart b/uni/lib/view/home/widgets/schedule_card_shimmer.dart index 506ac0621..c4e06838b 100644 --- a/uni/lib/view/home/widgets/schedule_card_shimmer.dart +++ b/uni/lib/view/home/widgets/schedule_card_shimmer.dart @@ -1,94 +1,74 @@ import 'package:flutter/material.dart'; -class ScheduleCardShimmer extends StatelessWidget { - const ScheduleCardShimmer({Key? key}) : super(key: key); - Widget _getSingleScheduleWidget(BuildContext context) { +class ScheduleCardShimmer extends StatelessWidget{ + const ScheduleCardShimmer({Key? key}) : super(key: key); + + Widget _getSingleScheduleWidget(BuildContext context){ return Center( - child: Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), child: Container( - margin: const EdgeInsets.only(top: 8, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), + margin: const EdgeInsets.only(top: 8.0), + child: Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox(height: 2.5,), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + + ], + ) + ]), Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - height: 2.5, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), + Container(height: 25, width: 100, color: Colors.black,), //UC section + const SizedBox(height: 10,), + Container(height: 15, width: 150, color: Colors.black,), //UC section + ], - ) - ]), - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 25, - width: 100, - color: Colors.black, - ), //UC section - const SizedBox( - height: 10, - ), - Container( - height: 15, - width: 150, - color: Colors.black, - ), //UC section - ], - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), //Room section - ], - )), - )); + ), + Container(height: 15, width: 40, color: Colors.black,), //Room section + ], + )), + )); } @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - height: 15, - width: 80, - color: Colors.black, - ), //Day of the week - const SizedBox( - height: 10, - ), + Container(height: 15, width: 80, color: Colors.black,), //Day of the week + const SizedBox(height: 10,), _getSingleScheduleWidget(context), _getSingleScheduleWidget(context), ], ); } -} +} \ No newline at end of file diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 0f4f1d7a1..228110583 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -94,13 +94,13 @@ class LibraryPage extends StatelessWidget { child: Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text('Piso ${floor.number}', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), Text('${floor.percentage}%', - style: Theme.of(context).textTheme.titleLarge), + style: Theme.of(context).textTheme.headline6), Text('${floor.occupation}/${floor.capacity}', style: Theme.of(context) .textTheme - .titleLarge + .headline6 ?.copyWith(color: Theme.of(context).colorScheme.background)), LinearPercentIndicator( lineHeight: 7.0, diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index bcaa96d43..966d9c71d 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -40,7 +40,7 @@ class LibraryOccupationCard extends GenericCard { if (occupation == null || occupation.capacity == 0) { return Center( child: Text('Não existem dados para apresentar', - style: Theme.of(context).textTheme.titleLarge, + style: Theme.of(context).textTheme.headline6, textAlign: TextAlign.center)); } return Padding( @@ -52,13 +52,13 @@ class LibraryOccupationCard extends GenericCard { center: Text('${occupation.percentage}%', style: Theme.of(context) .textTheme - .displayMedium + .headline2 ?.copyWith(fontSize: 23, fontWeight: FontWeight.w500)), footer: Column( children: [ const Padding(padding: EdgeInsets.fromLTRB(0, 5.0, 0, 0)), Text('${occupation.occupation}/${occupation.capacity}', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), ], ), circularStrokeCap: CircularStrokeCap.square, diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index b9ce8722c..5c49691b6 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -26,6 +26,11 @@ class LocationsPageState extends GeneralPageViewState super.initState(); } + @override + void dispose() { + super.dispose(); + } + @override Widget getBody(BuildContext context) { return Consumer( diff --git a/uni/lib/view/locations/widgets/faculty_maps.dart b/uni/lib/view/locations/widgets/faculty_maps.dart index 7d113e654..5d6287a48 100644 --- a/uni/lib/view/locations/widgets/faculty_maps.dart +++ b/uni/lib/view/locations/widgets/faculty_maps.dart @@ -22,8 +22,8 @@ class FacultyMaps { ); } - static getFontColor(BuildContext context) { - return Theme.of(context).brightness == Brightness.light + static getFontColor(BuildContext context){ + return Theme.of(context).brightness == Brightness.light ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.tertiary; } diff --git a/uni/lib/view/locations/widgets/floorless_marker_popup.dart b/uni/lib/view/locations/widgets/floorless_marker_popup.dart index c7129ab87..419787ac0 100644 --- a/uni/lib/view/locations/widgets/floorless_marker_popup.dart +++ b/uni/lib/view/locations/widgets/floorless_marker_popup.dart @@ -15,7 +15,7 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { final List locations = locationGroup.floors.values.expand((x) => x).toList(); return Card( - color: Theme.of(context).colorScheme.background.withOpacity(0.8), + color: Theme.of(context).backgroundColor.withOpacity(0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), diff --git a/uni/lib/view/locations/widgets/icons.dart b/uni/lib/view/locations/widgets/icons.dart index 7e3d41972..b1958ac3d 100644 --- a/uni/lib/view/locations/widgets/icons.dart +++ b/uni/lib/view/locations/widgets/icons.dart @@ -11,7 +11,7 @@ /// fonts: /// - asset: fonts/LocationIcons.ttf /// -/// +/// /// import 'package:flutter/widgets.dart'; @@ -22,13 +22,13 @@ class LocationIcons { static const String? _kFontPkg = null; static const IconData bookOpenBlankVariant = - IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData bottleSodaClassic = - IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData cashMultiple = - IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData coffee = - IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData printer = - IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index 6eb951323..3e281df5b 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -59,10 +59,12 @@ class LocationsMap extends StatelessWidget { ) ], children: [ - TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], - tileProvider: CachedTileProvider(), + TileLayerWidget( + options: TileLayerOptions( + urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + tileProvider: CachedTileProvider(), + ), ), PopupMarkerLayerWidget( options: PopupMarkerLayerOptions( @@ -94,7 +96,7 @@ class CachedTileProvider extends TileProvider { CachedTileProvider(); @override - ImageProvider getImage(Coords coords, TileLayer options) { + ImageProvider getImage(Coords coords, TileLayerOptions options) { return CachedNetworkImageProvider( getTileUrl(coords, options), ); diff --git a/uni/lib/view/locations/widgets/marker.dart b/uni/lib/view/locations/widgets/marker.dart index d3cca2d33..677efed26 100644 --- a/uni/lib/view/locations/widgets/marker.dart +++ b/uni/lib/view/locations/widgets/marker.dart @@ -17,7 +17,7 @@ class LocationMarker extends Marker { point: latlng, builder: (BuildContext ctx) => Container( decoration: BoxDecoration( - color: Theme.of(ctx).colorScheme.background, + color: Theme.of(ctx).backgroundColor, border: Border.all( color: Theme.of(ctx).colorScheme.primary, ), diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 87b653fd8..0af9b1eb2 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -13,7 +13,10 @@ class LocationMarkerPopup extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: Theme.of(context).colorScheme.background.withOpacity(0.8), + color: Theme + .of(context) + .backgroundColor + .withOpacity(0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), @@ -23,8 +26,8 @@ class LocationMarkerPopup extends StatelessWidget { direction: Axis.vertical, spacing: 8, children: (showId - ? [Text(locationGroup.id.toString())] - : []) + + ? [Text(locationGroup.id.toString())] + : []) + buildFloors(context), )), ); @@ -33,7 +36,7 @@ class LocationMarkerPopup extends StatelessWidget { List buildFloors(BuildContext context) { //Sort by floor final List>> entries = - locationGroup.floors.entries.toList(); + locationGroup.floors.entries.toList(); entries.sort((current, next) => -current.key.compareTo(next.key)); return entries.map((entry) { @@ -44,28 +47,28 @@ class LocationMarkerPopup extends StatelessWidget { }).toList(); } - List buildFloor( - BuildContext context, floor, List locations) { + List buildFloor(BuildContext context, floor, + List locations) { final Color fontColor = FacultyMaps.getFontColor(context); final String floorString = - 0 <= floor && floor <= 9 //To maintain layout of popup - ? ' $floor' - : '$floor'; + 0 <= floor && floor <= 9 //To maintain layout of popup + ? ' $floor' + : '$floor'; final Widget floorCol = Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: - Text('Andar $floorString', style: TextStyle(color: fontColor))) + child: Text( + 'Andar $floorString', style: TextStyle(color: fontColor))) ], ); final Widget locationsColumn = Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), decoration: - BoxDecoration(border: Border(left: BorderSide(color: fontColor))), + BoxDecoration(border: Border(left: BorderSide(color: fontColor))), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -74,16 +77,17 @@ class LocationMarkerPopup extends StatelessWidget { return [floorCol, locationsColumn]; } - List buildLocations( - BuildContext context, List locations, Color color) { + List buildLocations(BuildContext context, List locations, + Color color) { return locations - .map((location) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(location.description(), - textAlign: TextAlign.left, style: TextStyle(color: color)) - ], - )) + .map((location) => + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(location.description(), + textAlign: TextAlign.left, style: TextStyle(color: color)) + ], + )) .toList(); } } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 571e6b2a4..c74b1b73a 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -164,8 +164,7 @@ class LoginPageViewState extends State { width: 100.0, child: SvgPicture.asset( 'assets/images/logo_dark.svg', - colorFilter: - const ColorFilter.mode(Colors.white, BlendMode.srcIn), + color: Colors.white, )), ])); } @@ -200,7 +199,7 @@ class LoginPageViewState extends State { return InkWell( child: Center( child: Text("Esqueceu a palavra-passe?", - style: Theme.of(context).textTheme.bodyLarge!.copyWith( + style: Theme.of(context).textTheme.bodyText1!.copyWith( decoration: TextDecoration.underline, color: Colors.white))), onTap: () => launchUrl(Uri.parse("https://self-id.up.pt/reset"))); diff --git a/uni/lib/view/login/widgets/faculties_selection_form.dart b/uni/lib/view/login/widgets/faculties_selection_form.dart index 7ce700eb8..6949317c8 100644 --- a/uni/lib/view/login/widgets/faculties_selection_form.dart +++ b/uni/lib/view/login/widgets/faculties_selection_form.dart @@ -33,8 +33,7 @@ class _FacultiesSelectionFormState extends State { child: const Text('Cancelar', style: TextStyle(color: Colors.white))), ElevatedButton( style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, - backgroundColor: Colors.white), + foregroundColor: Theme.of(context).primaryColor, backgroundColor: Colors.white), onPressed: () { if (widget.selectedFaculties.isEmpty) { ToastMessage.warning( diff --git a/uni/lib/view/navigation_service.dart b/uni/lib/view/navigation_service.dart index 89bf885b2..8a163b060 100644 --- a/uni/lib/view/navigation_service.dart +++ b/uni/lib/view/navigation_service.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:uni/utils/drawer_items.dart'; + /// Manages the navigation logic class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); static logout() { - navigatorKey.currentState!.pushNamedAndRemoveUntil( - '/${DrawerItem.navLogOut.title}', (_) => false); + navigatorKey.currentState! + .pushNamedAndRemoveUntil('/${DrawerItem.navLogOut.title}', (_) => false); } } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 47a6363f5..e690f0baf 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -28,7 +28,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 20.0, bottom: 8.0, left: 20.0), child: Text('Saldo: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: const EdgeInsets.only( @@ -40,7 +40,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 8.0, bottom: 20.0, left: 20.0), child: Text('Data limite próxima prestação: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: const EdgeInsets.only( @@ -52,7 +52,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), child: Text("Notificar próxima data limite: ", - style: Theme.of(context).textTheme.titleSmall) + style: Theme.of(context).textTheme.subtitle2) ), Container( margin: diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index 86fb0b052..4c2b14476 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -18,7 +18,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 20.0), child: Text('Ano curricular atual: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: @@ -30,7 +30,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Estado atual: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: @@ -42,7 +42,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Ano da primeira inscrição: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: @@ -57,7 +57,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Faculdade: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: @@ -68,8 +68,8 @@ class CourseInfoCard extends GenericCard { TableRow(children: [ Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: Text('Média: ', - style: Theme.of(context).textTheme.titleSmall), + child: + Text('Média: ', style: Theme.of(context).textTheme.subtitle2), ), Container( margin: @@ -83,7 +83,7 @@ class CourseInfoCard extends GenericCard { margin: const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), child: Text('ECTs realizados: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: diff --git a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart index c88dd2acb..612603931 100644 --- a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart +++ b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart @@ -34,7 +34,7 @@ Future addMoneyDialog(BuildContext context) async { child: Text( 'Os dados da referência gerada aparecerão no Sigarra, conta corrente. \nPerfil > Conta Corrente', textAlign: TextAlign.start, - style: Theme.of(context).textTheme.titleSmall)), + style: Theme.of(context).textTheme.subtitle2)), Row(children: [ IconButton( icon: const Icon(Icons.indeterminate_check_box), @@ -85,11 +85,11 @@ Future addMoneyDialog(BuildContext context) async { ], )), title: Text('Adicionar quota', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), + style: Theme.of(context).textTheme.bodyText2), onPressed: () => Navigator.pop(context)), ElevatedButton( onPressed: () => generateReference(context, value), diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index eb0155295..c30e87e37 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -31,13 +31,13 @@ class PrintInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 20.0, bottom: 20.0, left: 20.0), child: Text('Valor disponível: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme.of(context).textTheme.subtitle2), ), Container( margin: const EdgeInsets.only(right: 15.0), child: Text(profile.printBalance, textAlign: TextAlign.end, - style: Theme.of(context).textTheme.titleLarge)), + style: Theme.of(context).textTheme.headline6)), Container( margin: const EdgeInsets.only(right: 5.0), height: 30, diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 8d5144c51..3af637025 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -31,7 +31,8 @@ class _CanteenPageState extends GeneralPageViewState final int weekDay = DateTime.now().weekday; super.initState(); tabController = TabController(vsync: this, length: DayOfWeek.values.length); - tabController.animateTo((tabController.index + (weekDay-1))); + final offset = (weekDay > 5) ? 0 : (weekDay - 1) % DayOfWeek.values.length; + tabController.animateTo((tabController.index + offset)); scrollViewController = ScrollController(); } @@ -64,8 +65,7 @@ class _CanteenPageState extends GeneralPageViewState contentGenerator: createTabViewBuilder, content: restaurants, contentChecker: restaurants.isNotEmpty, - onNullContent: - const Center(child: Text('Não há refeições disponíveis.'))) + onNullContent: const Center(child: Text('Não há refeições disponíveis.'))) ]); } @@ -92,7 +92,7 @@ class _CanteenPageState extends GeneralPageViewState for (var i = 0; i < DayOfWeek.values.length; i++) { tabs.add(Container( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).backgroundColor, child: Tab(key: Key('cantine-page-tab-$i'), text: toString(DayOfWeek.values[i])), )); } @@ -101,8 +101,7 @@ class _CanteenPageState extends GeneralPageViewState } Widget createRestaurant(context, Restaurant restaurant, DayOfWeek dayOfWeek) { - return RestaurantPageCard( - restaurant.name, createRestaurantByDay(context, restaurant, dayOfWeek)); + return RestaurantPageCard(restaurant.name, createRestaurantByDay(context, restaurant, dayOfWeek)); } List createRestaurantRows(List meals, BuildContext context) { @@ -116,23 +115,25 @@ class _CanteenPageState extends GeneralPageViewState final List meals = restaurant.getMealsOfDay(day); if (meals.isEmpty) { return Container( - margin: const EdgeInsets.only(top: 10, bottom: 5), + margin: + const EdgeInsets.only(top: 10, bottom: 5), key: Key('cantine-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, - children: const [ - Center( - child: Text("Não há informação disponível sobre refeições")), - ], - )); + children: + const [Center (child: Text("Não há informação disponível sobre refeições")),], + ) + ); } else { return Container( - margin: const EdgeInsets.only(top: 5, bottom: 5), - key: Key('cantine-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: createRestaurantRows(meals, context), - )); + margin: + const EdgeInsets.only(top: 5, bottom: 5), + key: Key('cantine-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: createRestaurantRows(meals, context), + ) + ); } } } diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index 062fe8a88..9dbfd2773 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -5,9 +5,7 @@ class RestaurantPageCard extends GenericCard { final String restaurantName; final Widget meals; - RestaurantPageCard(this.restaurantName, this.meals, {super.key}) - : super.customStyle( - editingMode: false, onDelete: () => null, smallTitle: true); + RestaurantPageCard(this.restaurantName, this.meals, {super.key}) : super.customStyle(editingMode: false, onDelete: () => null, smallTitle: true); @override Widget buildCardContent(BuildContext context) { @@ -21,4 +19,4 @@ class RestaurantPageCard extends GenericCard { @override onClick(BuildContext context) {} -} +} \ No newline at end of file diff --git a/uni/lib/view/restaurant/widgets/restaurant_slot.dart b/uni/lib/view/restaurant/widgets/restaurant_slot.dart index 7f30d6de3..15ecc621e 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_slot.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_slot.dart @@ -59,8 +59,7 @@ class RestaurantSlot extends StatelessWidget { child: icon != '' ? SvgPicture.asset( icon, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), + color: Theme.of(context).primaryColor, height: 20, ) : null); diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 5104f4b0f..8fe37a05b 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -45,14 +45,13 @@ class SchedulePageView extends StatefulWidget { static final List daysOfTheWeek = TimeString.getWeekdaysStrings(includeWeekend: false); - static List> groupLecturesByDay(schedule) { - final aggLectures = >[]; + static List> groupLecturesByDay(schedule) { + final aggLectures = >[]; for (int i = 0; i < daysOfTheWeek.length; i++) { - final Set lectures = {}; + final List lectures = []; for (int j = 0; j < schedule.length; j++) { - if (schedule[j].startTime.weekday-1 == i) lectures.add(schedule[j]); - + if (schedule[j].day == i) lectures.add(schedule[j]); } aggLectures.add(lectures); } @@ -138,7 +137,6 @@ class SchedulePageViewState extends GeneralPageViewState /// Returns a list of widgets for the rows with a singular class info. List createScheduleRows(lectures, BuildContext context) { final List scheduleContent = []; - lectures = lectures.toList(); for (int i = 0; i < lectures.length; i++) { final Lecture lecture = lectures[i]; scheduleContent.add(ScheduleSlot( diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index 8e48c7d7f..89894ba32 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -7,8 +6,8 @@ import 'package:url_launcher/url_launcher.dart'; class ScheduleSlot extends StatelessWidget { final String subject; final String rooms; - final DateTime begin; - final DateTime end; + final String begin; + final String end; final String teacher; final String typeClass; final String? classNumber; @@ -52,14 +51,14 @@ class ScheduleSlot extends StatelessWidget { return Column( key: Key('schedule-slot-time-$begin-$end'), children: [ - createScheduleTime(DateFormat("HH:mm").format(begin), context), - createScheduleTime(DateFormat("HH:mm").format(end), context) + createScheduleTime(begin, context), + createScheduleTime(end, context) ], ); } Widget createScheduleTime(String time, context) => createTextField( - time, Theme.of(context).textTheme.bodyMedium, TextAlign.center); + time, Theme.of(context).textTheme.bodyText2, TextAlign.center); String toUcLink(int occurrId) { const String faculty = 'feup'; //should not be hardcoded @@ -96,13 +95,13 @@ class ScheduleSlot extends StatelessWidget { subject, Theme.of(context) .textTheme - .headlineSmall! + .headline5! .apply(color: Theme.of(context).colorScheme.tertiary), TextAlign.center); final typeClassTextField = createTextField(' ($typeClass)', - Theme.of(context).textTheme.bodyMedium, TextAlign.center); + Theme.of(context).textTheme.bodyText2, TextAlign.center); final roomTextField = createTextField( - rooms, Theme.of(context).textTheme.bodyMedium, TextAlign.right); + rooms, Theme.of(context).textTheme.bodyText2, TextAlign.right); return [ createScheduleSlotTime(context), Expanded( @@ -129,7 +128,7 @@ class ScheduleSlot extends StatelessWidget { Widget createScheduleSlotTeacherClassInfo(context) { return createTextField( classNumber != null ? '$classNumber | $teacher' : teacher, - Theme.of(context).textTheme.bodyMedium, + Theme.of(context).textTheme.bodyText2, TextAlign.center); } diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index caa578bc4..e1383ebff 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -84,17 +84,17 @@ class SplashScreenState extends State { ), child: SizedBox( width: 150.0, - child: SvgPicture.asset('assets/images/logo_dark.svg', - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn)))); + child: SvgPicture.asset( + 'assets/images/logo_dark.svg', + color: Theme.of(context).primaryColor, + ))); } /// Creates the app main logo Widget createNILogo(BuildContext context) { return SvgPicture.asset( 'assets/images/by_niaefeup.svg', - colorFilter: - ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), + color: Theme.of(context).primaryColor, width: queryData.size.width * 0.45, ); } diff --git a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart index 81abb0377..0b5c3557f 100644 --- a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart +++ b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart @@ -38,7 +38,7 @@ class TermsAndConditionDialog { builder: (BuildContext context) { return AlertDialog( title: Text('Mudança nos Termos e Condições da uni', - style: Theme.of(context).textTheme.headlineSmall), + style: Theme.of(context).textTheme.headline5), content: Column( children: [ Expanded( @@ -91,6 +91,6 @@ class TermsAndConditionDialog { } static TextStyle getTextMethod(BuildContext context) { - return Theme.of(context).textTheme.titleLarge!; + return Theme.of(context).textTheme.headline6!; } } diff --git a/uni/lib/view/theme.dart b/uni/lib/view/theme.dart index 8684afef8..f58691c14 100644 --- a/uni/lib/view/theme.dart +++ b/uni/lib/view/theme.dart @@ -5,30 +5,31 @@ const Color lightRed = Color.fromARGB(255, 180, 30, 30); const Color _mildWhite = Color.fromARGB(255, 0xfa, 0xfa, 0xfa); const Color _lightGrey = Color.fromARGB(255, 215, 215, 215); +const Color _grey = Color.fromARGB(255, 0x7f, 0x7f, 0x7f); const Color _strongGrey = Color.fromARGB(255, 90, 90, 90); const Color _mildBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkishBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkBlack = Color.fromARGB(255, 27, 27, 27); const _textTheme = TextTheme( - displayLarge: TextStyle(fontSize: 40.0, fontWeight: FontWeight.w400), - displayMedium: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400), - displaySmall: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400), - headlineMedium: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w300), - headlineSmall: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w400), - titleLarge: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300), - titleMedium: TextStyle(fontSize: 17.0, fontWeight: FontWeight.w300), - titleSmall: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w300), - bodyLarge: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400), - bodyMedium: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - bodySmall: TextStyle(fontSize: 13.0, fontWeight: FontWeight.w400), + headline1: TextStyle(fontSize: 40.0, fontWeight: FontWeight.w400), + headline2: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400), + headline3: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400), + headline4: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w300), + headline5: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w400), + headline6: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300), + subtitle1: TextStyle(fontSize: 17.0, fontWeight: FontWeight.w300), + subtitle2: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w300), + bodyText1: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400), + bodyText2: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), + caption: TextStyle(fontSize: 13.0, fontWeight: FontWeight.w400), ); ThemeData applicationLightTheme = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: darkRed, brightness: Brightness.light, - background: _mildWhite, + background: _grey, primary: darkRed, onPrimary: Colors.white, secondary: darkRed, @@ -38,43 +39,27 @@ ThemeData applicationLightTheme = ThemeData( brightness: Brightness.light, primaryColor: darkRed, textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, + selectionHandleColor: Colors.transparent, ), canvasColor: _mildWhite, + backgroundColor: _mildWhite, scaffoldBackgroundColor: _mildWhite, cardColor: Colors.white, hintColor: _lightGrey, dividerColor: _lightGrey, indicatorColor: darkRed, primaryTextTheme: Typography().black.copyWith( - headlineMedium: const TextStyle(color: _strongGrey), - bodyLarge: const TextStyle(color: _strongGrey)), + headline4: const TextStyle(color: _strongGrey), + bodyText1: const TextStyle(color: _strongGrey)), + toggleableActiveColor: darkRed, iconTheme: const IconThemeData(color: darkRed), - textTheme: _textTheme, - switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, - ), - trackColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, - ), - ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, - ), - ), - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, - ), - )); + textTheme: _textTheme); ThemeData applicationDarkTheme = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: lightRed, brightness: Brightness.dark, - background: _darkBlack, + background: _grey, primary: _lightGrey, onPrimary: _darkishBlack, secondary: _lightGrey, @@ -83,30 +68,17 @@ ThemeData applicationDarkTheme = ThemeData( onTertiary: _darkishBlack), brightness: Brightness.dark, textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, + selectionHandleColor: Colors.transparent, ), primaryColor: _lightGrey, canvasColor: _darkBlack, + backgroundColor: _darkBlack, scaffoldBackgroundColor: _darkBlack, cardColor: _mildBlack, hintColor: _darkishBlack, dividerColor: _strongGrey, indicatorColor: _lightGrey, primaryTextTheme: Typography().white, + toggleableActiveColor: _mildBlack, iconTheme: const IconThemeData(color: _lightGrey), - textTheme: _textTheme.apply(bodyColor: _lightGrey), - switchTheme: SwitchThemeData( - trackColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _lightGrey : null, - ), - ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, - ), - ), - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, - ), - )); + textTheme: _textTheme.apply(bodyColor: _lightGrey)); diff --git a/uni/lib/view/useful_info/widgets/link_button.dart b/uni/lib/view/useful_info/widgets/link_button.dart index f333eaa00..230668485 100644 --- a/uni/lib/view/useful_info/widgets/link_button.dart +++ b/uni/lib/view/useful_info/widgets/link_button.dart @@ -22,7 +22,7 @@ class LinkButton extends StatelessWidget { child: Text(title, style: Theme.of(context) .textTheme - .headlineSmall! + .headline5! .copyWith(decoration: TextDecoration.underline)), onTap: () => launchUrl(Uri.parse(link)), )) diff --git a/uni/lib/view/useful_info/widgets/text_components.dart b/uni/lib/view/useful_info/widgets/text_components.dart index 4858559cf..9c70eb709 100644 --- a/uni/lib/view/useful_info/widgets/text_components.dart +++ b/uni/lib/view/useful_info/widgets/text_components.dart @@ -9,8 +9,7 @@ Container h1(String text, BuildContext context, {bool initial = false}) { alignment: Alignment.centerLeft, child: Opacity( opacity: 0.8, - child: - Text(text, style: Theme.of(context).textTheme.headlineSmall)), + child: Text(text, style: Theme.of(context).textTheme.headline5)), )); } @@ -19,7 +18,7 @@ Container h2(String text, BuildContext context) { margin: const EdgeInsets.only(top: 13.0, bottom: 0.0, left: 20.0), child: Align( alignment: Alignment.centerLeft, - child: Text(text, style: Theme.of(context).textTheme.titleSmall), + child: Text(text, style: Theme.of(context).textTheme.subtitle2), )); } @@ -35,7 +34,7 @@ Container infoText(String text, BuildContext context, text, style: Theme.of(context) .textTheme - .bodyLarge! + .bodyText1! .apply(color: Theme.of(context).colorScheme.tertiary), ), onTap: () => link != '' ? launchUrl(Uri.parse(link)) : null), diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index a559742ed..fa76beef5 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,18 +20,18 @@ publish_to: 'none' # Remove this line if you wish to 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.5.20+138 +version: 1.5.14+132 environment: sdk: ">=2.17.1 <3.0.0" - flutter: 3.7.2 + flutter: 3.3.2 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -43,12 +43,12 @@ dependencies: encrypt: ^5.0.0-beta.1 path_provider: ^2.0.0 sqflite: ^2.0.3 - path: ^1.8.0 + path: ^1.8.0 cached_network_image: ^3.0.0-nullsafety - flutter_svg: ^2.0.0+1 + flutter_svg: ^1.1.0 synchronized: ^3.0.0 image: ^4.0.13 - connectivity_plus: ^3.0.3 + connectivity_plus: ^3.0.2 logger: ^1.1.0 url_launcher: ^6.0.2 flutter_markdown: ^0.6.0 @@ -61,12 +61,12 @@ dependencies: expansion_tile_card: ^2.0.0 collection: ^1.16.0 timelines: ^0.1.0 - flutter_map: ^3.1.0 + flutter_map: ^2.2.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 latlong2: ^0.8.1 - flutter_map_marker_popup: ^4.0.1 + flutter_map_marker_popup: ^3.2.0 workmanager: ^0.5.1 flutter_local_notifications: ^12.0.4 percent_indicator: ^4.2.2 From 9da07d5887a20d55165d0a1001b0d6614f41ff16 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Mon, 26 Jun 2023 20:31:01 +0100 Subject: [PATCH 025/100] Fixing test error --- uni/app_version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 7087f1922..1538c8e29 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.14+132 \ No newline at end of file +1.5.20+138 \ No newline at end of file From eea9bd8a772ab6d4ef48118a210734d2086c39e6 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 30 Jun 2023 00:38:23 +0100 Subject: [PATCH 026/100] Fixing files management --- .github/workflows/deploy.yaml | 2 +- .github/workflows/test_lint.yaml | 4 +- uni/android/app/src/main/res/raw/keep.xml | 2 + .../background_workers/notifications.dart | 2 +- .../notifications/tuition_notification.dart | 25 +++- .../local_storage/app_bus_stop_database.dart | 2 +- .../local_storage/app_courses_database.dart | 1 + .../local_storage/app_lectures_database.dart | 10 +- .../local_storage/app_shared_preferences.dart | 9 +- .../notification_timeout_storage.dart | 4 +- uni/lib/controller/logout.dart | 1 - .../controller/networking/network_router.dart | 10 +- .../controller/parsers/parser_calendar.dart | 10 +- uni/lib/controller/parsers/parser_exams.dart | 9 +- .../controller/parsers/parser_schedule.dart | 12 +- .../parsers/parser_schedule_html.dart | 23 ++- uni/lib/model/entities/bug_report.dart | 28 ++-- uni/lib/model/entities/calendar_event.dart | 5 +- uni/lib/model/entities/exam.dart | 10 +- uni/lib/model/entities/lecture.dart | 75 +++------- uni/lib/model/entities/time_utilities.dart | 15 +- uni/lib/utils/duration_string_formatter.dart | 46 ++++++ uni/lib/view/about/about.dart | 3 +- uni/lib/view/bug_report/widgets/form.dart | 34 +++-- .../view/bug_report/widgets/text_field.dart | 6 +- .../widgets/bus_stop_row.dart | 4 +- .../widgets/estimated_arrival_timestamp.dart | 3 +- .../widgets/trip_row.dart | 6 +- .../widgets/bus_stop_search.dart | 4 +- uni/lib/view/calendar/calendar.dart | 4 +- .../view/common_widgets/date_rectangle.dart | 2 +- uni/lib/view/common_widgets/generic_card.dart | 10 +- .../generic_expansion_card.dart | 2 +- .../common_widgets/last_update_timestamp.dart | 2 +- uni/lib/view/common_widgets/page_title.dart | 4 +- .../pages_layouts/general/general.dart | 3 +- .../general/widgets/navigation_drawer.dart | 2 +- .../request_dependent_widget_builder.dart | 16 ++- .../view/common_widgets/toast_message.dart | 4 +- uni/lib/view/course_units/course_units.dart | 4 +- uni/lib/view/exams/widgets/day_title.dart | 2 +- .../view/exams/widgets/exam_filter_form.dart | 7 +- uni/lib/view/exams/widgets/exam_row.dart | 9 +- uni/lib/view/exams/widgets/exam_time.dart | 6 +- uni/lib/view/exams/widgets/exam_title.dart | 9 +- uni/lib/view/home/widgets/bus_stop_card.dart | 6 +- uni/lib/view/home/widgets/exam_card.dart | 6 +- .../view/home/widgets/exam_card_shimmer.dart | 131 ++++++++++-------- .../view/home/widgets/exit_app_dialog.dart | 2 +- .../view/home/widgets/main_cards_list.dart | 6 +- .../view/home/widgets/restaurant_card.dart | 2 +- uni/lib/view/home/widgets/schedule_card.dart | 31 ++--- .../home/widgets/schedule_card_shimmer.dart | 124 ++++++++++------- uni/lib/view/library/library.dart | 6 +- .../widgets/library_occupation_card.dart | 6 +- uni/lib/view/locations/locations.dart | 5 - .../view/locations/widgets/faculty_maps.dart | 4 +- .../widgets/floorless_marker_popup.dart | 2 +- uni/lib/view/locations/widgets/icons.dart | 12 +- uni/lib/view/locations/widgets/map.dart | 12 +- uni/lib/view/locations/widgets/marker.dart | 2 +- .../view/locations/widgets/marker_popup.dart | 46 +++--- uni/lib/view/login/login.dart | 5 +- .../widgets/faculties_selection_form.dart | 3 +- uni/lib/view/navigation_service.dart | 5 +- .../profile/widgets/account_info_card.dart | 6 +- .../profile/widgets/course_info_card.dart | 14 +- .../widgets/create_print_mb_dialog.dart | 6 +- .../view/profile/widgets/print_info_card.dart | 4 +- .../view/restaurant/restaurant_page_view.dart | 37 +++-- .../widgets/restaurant_page_card.dart | 6 +- .../restaurant/widgets/restaurant_slot.dart | 3 +- .../view/schedule/widgets/schedule_slot.dart | 19 +-- uni/lib/view/splash/splash.dart | 10 +- .../widgets/terms_and_condition_dialog.dart | 4 +- uni/lib/view/theme.dart | 76 ++++++---- .../view/useful_info/widgets/link_button.dart | 2 +- .../useful_info/widgets/text_components.dart | 7 +- uni/pubspec.yaml | 16 +-- 79 files changed, 594 insertions(+), 483 deletions(-) create mode 100644 uni/android/app/src/main/res/raw/keep.xml create mode 100644 uni/lib/utils/duration_string_formatter.dart diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7ebeb3272..6289971b7 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -40,7 +40,7 @@ jobs: env: PROPERTIES_PATH: "android/key.properties" JAVA_VERSION: "11.x" - FLUTTER_VERSION: "3.3.2" + FLUTTER_VERSION: "3.7.2" defaults: run: working-directory: ./uni diff --git a/.github/workflows/test_lint.yaml b/.github/workflows/test_lint.yaml index d8ef6e30e..ffb255569 100644 --- a/.github/workflows/test_lint.yaml +++ b/.github/workflows/test_lint.yaml @@ -14,7 +14,7 @@ jobs: java-version: '11.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.3.2' + flutter-version: '3.7.2' - name: Cache pub dependencies uses: actions/cache@v2 @@ -39,7 +39,7 @@ jobs: java-version: '11.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.3.2' + flutter-version: '3.7.2' - run: flutter pub get - run: flutter test --no-sound-null-safety diff --git a/uni/android/app/src/main/res/raw/keep.xml b/uni/android/app/src/main/res/raw/keep.xml new file mode 100644 index 000000000..7ebdf53a0 --- /dev/null +++ b/uni/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,2 @@ + + diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index ed69bdfd1..a270fa644 100644 --- a/uni/lib/controller/background_workers/notifications.dart +++ b/uni/lib/controller/background_workers/notifications.dart @@ -76,7 +76,7 @@ class NotificationManager { if (lastRan.add(notification.timeout).isBefore(DateTime.now())) { await notification.displayNotificationIfPossible( session, _localNotificationsPlugin); - notificationStorage.addLastTimeNotificationExecuted( + await notificationStorage.addLastTimeNotificationExecuted( notification.uniqueID, DateTime.now()); } } diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 469fa2661..777084d75 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -5,6 +5,7 @@ import 'package:uni/controller/fetchers/fees_fetcher.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/model/entities/session.dart'; +import 'package:uni/utils/duration_string_formatter.dart'; class TuitionNotification extends Notification { late DateTime _dueDate; @@ -17,13 +18,25 @@ class TuitionNotification extends Notification { Session session) async { //We must add one day because the time limit is actually at 23:59 and not at 00:00 of the same day if (_dueDate.add(const Duration(days: 1)).isBefore(DateTime.now())) { - final int days = DateTime.now().difference(_dueDate).inDays; - return Tuple2("⚠️ Ainda não pagaste as propinas ⚠️", - "Já passaram $days dias desde o dia limite"); + final Duration duration = DateTime.now().difference(_dueDate); + if (duration.inDays == 0) { + return const Tuple2("⚠️ Ainda não pagaste as propinas ⚠️", + "O prazo para pagar as propinas acabou ontem"); + } + return Tuple2( + "⚠️ Ainda não pagaste as propinas ⚠️", + duration.toFormattedString("Já passou {} desde a data limite", + "Já passaram {} desde a data limite")); } - final int days = _dueDate.difference(DateTime.now()).inDays; - return Tuple2("O prazo limite para as propinas está a acabar", - "Faltam $days dias para o prazo acabar"); + final Duration duration = _dueDate.difference(DateTime.now()); + if (duration.inDays == 0) { + return const Tuple2("O prazo limite para as propinas está a acabar", + "Hoje acaba o prazo para pagamento das propinas!"); + } + return Tuple2( + "O prazo limite para as propinas está a acabar", + duration.toFormattedString( + "Falta {} para a data limite", "Faltam {} para a data limite")); } @override diff --git a/uni/lib/controller/local_storage/app_bus_stop_database.dart b/uni/lib/controller/local_storage/app_bus_stop_database.dart index 2c31300d1..e13fdbe92 100644 --- a/uni/lib/controller/local_storage/app_bus_stop_database.dart +++ b/uni/lib/controller/local_storage/app_bus_stop_database.dart @@ -76,7 +76,7 @@ class AppBusStopDatabase extends AppDatabase { Future _insertBusStops(Map stops) async { stops.forEach((stopCode, stopData) async { await insertInDatabase('favoritestops', - {'stopCode': stopCode, 'favorited': stopData.favorited.toString()}); + {'stopCode': stopCode, 'favorited': stopData.favorited ? '1' : '0'}); for (var busCode in stopData.configuredBuses) { await insertInDatabase( 'busstops', diff --git a/uni/lib/controller/local_storage/app_courses_database.dart b/uni/lib/controller/local_storage/app_courses_database.dart index 650cc5640..f85e35b44 100644 --- a/uni/lib/controller/local_storage/app_courses_database.dart +++ b/uni/lib/controller/local_storage/app_courses_database.dart @@ -71,5 +71,6 @@ class AppCoursesDatabase extends AppDatabase { final batch = db.batch(); batch.execute('DROP TABLE IF EXISTS courses'); batch.execute(createScript); + await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_lectures_database.dart b/uni/lib/controller/local_storage/app_lectures_database.dart index 079d7a21c..d4ec97b53 100644 --- a/uni/lib/controller/local_storage/app_lectures_database.dart +++ b/uni/lib/controller/local_storage/app_lectures_database.dart @@ -10,7 +10,7 @@ import 'package:sqflite/sqflite.dart'; class AppLecturesDatabase extends AppDatabase { static const createScript = '''CREATE TABLE lectures(subject TEXT, typeClass TEXT, - day INTEGER, startTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; + startDateTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; AppLecturesDatabase() : super( @@ -19,7 +19,7 @@ class AppLecturesDatabase extends AppDatabase { createScript, ], onUpgrade: migrate, - version: 5); + version: 6); /// Replaces all of the data in this database with [lecs]. saveNewLectures(List lecs) async { @@ -33,11 +33,10 @@ class AppLecturesDatabase extends AppDatabase { final List> maps = await db.query('lectures'); return List.generate(maps.length, (i) { - return Lecture.fromHtml( + return Lecture.fromApi( maps[i]['subject'], maps[i]['typeClass'], - maps[i]['day'], - maps[i]['startTime'], + maps[i]['startDateTime'], maps[i]['blocks'], maps[i]['room'], maps[i]['teacher'], @@ -77,5 +76,6 @@ class AppLecturesDatabase extends AppDatabase { final batch = db.batch(); batch.execute('DROP TABLE IF EXISTS lectures'); batch.execute(createScript); + await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/app_shared_preferences.dart index 63aec56ec..9134e9ef4 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/app_shared_preferences.dart @@ -7,7 +7,6 @@ import 'package:tuple/tuple.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/utils/favorite_widget_type.dart'; - /// Manages the app's Shared Preferences. /// /// This database stores the user's student number, password and favorite @@ -151,18 +150,18 @@ class AppSharedPreferences { .toList(); } - static saveHiddenExams(List newHiddenExams) async { final prefs = await SharedPreferences.getInstance(); - prefs.setStringList( - hiddenExams, newHiddenExams); + prefs.setStringList(hiddenExams, newHiddenExams); } static Future> getHiddenExams() async { final prefs = await SharedPreferences.getInstance(); - final List storedHiddenExam = prefs.getStringList(hiddenExams) ?? []; + final List storedHiddenExam = + prefs.getStringList(hiddenExams) ?? []; return storedHiddenExam; } + /// Replaces the user's exam filter settings with [newFilteredExamTypes]. static saveFilteredExams(Map newFilteredExamTypes) async { final prefs = await SharedPreferences.getInstance(); diff --git a/uni/lib/controller/local_storage/notification_timeout_storage.dart b/uni/lib/controller/local_storage/notification_timeout_storage.dart index 6a36bc427..4f9173d8e 100644 --- a/uni/lib/controller/local_storage/notification_timeout_storage.dart +++ b/uni/lib/controller/local_storage/notification_timeout_storage.dart @@ -36,8 +36,8 @@ class NotificationTimeoutStorage{ return DateTime.parse(_fileContent[uniqueID]); } - void addLastTimeNotificationExecuted(String uniqueID, DateTime lastRan) async{ - _fileContent.putIfAbsent(uniqueID, () => lastRan.toString()); + Future addLastTimeNotificationExecuted(String uniqueID, DateTime lastRan) async{ + _fileContent[uniqueID] = lastRan.toIso8601String(); await _writeToFile(await _getTimeoutFile()); } diff --git a/uni/lib/controller/logout.dart b/uni/lib/controller/logout.dart index e6b71ad13..65e4d3965 100644 --- a/uni/lib/controller/logout.dart +++ b/uni/lib/controller/logout.dart @@ -16,7 +16,6 @@ import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; - Future logout(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); final faculties = await AppSharedPreferences.getUserFaculties(); diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index c3dfec1fa..9899be172 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -178,13 +178,13 @@ class NetworkRouter { /// Makes an HTTP request to terminate the session in Sigarra. static Future killAuthentication(List faculties) async { - final url = - '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; - final response = await http. - get(url.toUri()).timeout(const Duration(seconds: loginRequestTimeout)); + final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; + final response = await http + .get(url.toUri()) + .timeout(const Duration(seconds: loginRequestTimeout)); if (response.statusCode == 200) { Logger().i("Logout Successful"); - }else{ + } else { Logger().i("Logout Failed"); } return response; diff --git a/uni/lib/controller/parsers/parser_calendar.dart b/uni/lib/controller/parsers/parser_calendar.dart index 27dc81110..497da6c41 100644 --- a/uni/lib/controller/parsers/parser_calendar.dart +++ b/uni/lib/controller/parsers/parser_calendar.dart @@ -7,10 +7,10 @@ import 'package:uni/model/entities/calendar_event.dart'; Future> getCalendarFromHtml(Response response) async { final document = parse(response.body); - final List calendarHtml = - document.querySelectorAll('tr'); + final List calendarHtml = document.querySelectorAll('tr'); - return calendarHtml.map((event) => - CalendarEvent(event.children[0].innerHtml, event.children[1].innerHtml) - ).toList(); + return calendarHtml + .map((event) => CalendarEvent( + event.children[0].innerHtml, event.children[1].innerHtml)) + .toList(); } diff --git a/uni/lib/controller/parsers/parser_exams.dart b/uni/lib/controller/parsers/parser_exams.dart index 74491601c..aa7661f5e 100644 --- a/uni/lib/controller/parsers/parser_exams.dart +++ b/uni/lib/controller/parsers/parser_exams.dart @@ -46,8 +46,8 @@ class ParserExams { exams.querySelectorAll('td.exame').forEach((Element examsDay) { if (examsDay.querySelector('a') != null) { subject = examsDay.querySelector('a')!.text; - id = Uri.parse(examsDay.querySelector('a')!.attributes['href']!).queryParameters['p_exa_id']!; - + id = Uri.parse(examsDay.querySelector('a')!.attributes['href']!) + .queryParameters['p_exa_id']!; } if (examsDay.querySelector('span.exame-sala') != null) { rooms = @@ -60,8 +60,8 @@ class ParserExams { DateTime.parse('${dates[days]} ${splittedSchedule[0]}'); final DateTime end = DateTime.parse('${dates[days]} ${splittedSchedule[1]}'); - final Exam exam = - Exam(id,begin, end, subject ?? '', rooms, examTypes[tableNum],course.faculty!); + final Exam exam = Exam(id, begin, end, subject ?? '', rooms, + examTypes[tableNum], course.faculty!); examsList.add(exam); }); @@ -73,5 +73,4 @@ class ParserExams { }); return examsList; } - } diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 5517042ca..4d83e9bb9 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/lecture.dart'; +import 'package:uni/model/entities/time_utilities.dart'; Future> parseScheduleMultipleRequests(responses) async { List lectures = []; @@ -20,6 +21,7 @@ Future> parseSchedule(http.Response response) async { final json = jsonDecode(response.body); + final schedule = json['horario']; for (var lecture in schedule) { @@ -34,12 +36,16 @@ Future> parseSchedule(http.Response response) async { final String classNumber = lecture['turma_sigla']; final int occurrId = lecture['ocorrencia_id']; - lectures.add(Lecture.fromApi(subject, typeClass, day, secBegin, blocks, - room, teacher, classNumber, occurrId)); + final DateTime monday = DateTime.now().getClosestMonday(); + + final Lecture lec = Lecture.fromApi(subject, typeClass, monday.add(Duration(days:day, seconds: secBegin)), blocks, + room, teacher, classNumber, occurrId); + + lectures.add(lec); + } final lecturesList = lectures.toList(); - lecturesList.sort((a, b) => a.compare(b)); if (lecturesList.isEmpty) { diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index 788a6689c..428bbf98b 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:http/http.dart' as http; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart'; @@ -8,11 +7,16 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/entities/time_utilities.dart'; + + Future> getOverlappedClasses( Session session, Document document) async { final List lecturesList = []; + final DateTime monday = DateTime.now().getClosestMonday(); + final overlappingClasses = document.querySelectorAll('.dados > tbody > .d'); for (final element in overlappingClasses) { final String? subject = element.querySelector('acronym > a')?.text; @@ -34,7 +38,12 @@ Future> getOverlappedClasses( final String? classNumber = element.querySelector('td[headers=t6] > a')?.text; + try { + final DateTime fullStartTime = monday.add(Duration( + days: day, + hours: int.parse(startTime!.substring(0, 2)), + minutes: int.parse(startTime.substring(3, 5)))); final String? link = element.querySelector('td[headers=t6] > a')?.attributes['href']; @@ -45,14 +54,14 @@ Future> getOverlappedClasses( await NetworkRouter.getWithCookies(link, {}, session); final classLectures = await getScheduleFromHtml(response, session); + lecturesList.add(classLectures .where((element) => element.subject == subject && - startTime?.replaceFirst(':', 'h') == element.startTime && - element.day == day) + element.startTime == fullStartTime) .first); } catch (e) { - final Lecture lect = Lecture.fromHtml(subject!, typeClass!, day, + final Lecture lect = Lecture.fromHtml(subject!, typeClass!, monday.add(Duration(days: day)), startTime!, 0, room!, teacher!, classNumber!, -1); lecturesList.add(lect); } @@ -70,6 +79,10 @@ Future> getScheduleFromHtml( var semana = [0, 0, 0, 0, 0, 0]; final List lecturesList = []; + + final DateTime monday = DateTime.now().getClosestMonday(); + + document.querySelectorAll('.horario > tbody > tr').forEach((Element element) { if (element.getElementsByClassName('horas').isNotEmpty) { var day = 0; @@ -107,7 +120,7 @@ Future> getScheduleFromHtml( final Lecture lect = Lecture.fromHtml( subject, typeClass, - day, + monday.add(Duration(days: day)), startTime, blocks, room ?? '', diff --git a/uni/lib/model/entities/bug_report.dart b/uni/lib/model/entities/bug_report.dart index 3ef4b22d7..9596c7eeb 100644 --- a/uni/lib/model/entities/bug_report.dart +++ b/uni/lib/model/entities/bug_report.dart @@ -1,24 +1,18 @@ /// Stores information about Bug Report import 'package:tuple/tuple.dart'; -class BugReport{ +class BugReport { final String title; final String text; final String email; - final Tuple2? bugLabel; + final Tuple2? bugLabel; final List faculties; - BugReport( - this.title, - this.text, - this.email, - this.bugLabel, - this.faculties - ); - Map toMap() => { - 'title':title, - 'text':text, - 'email':email, - 'bugLabel':bugLabel!.item2, - 'faculties':faculties - }; -} \ No newline at end of file + BugReport(this.title, this.text, this.email, this.bugLabel, this.faculties); + Map toMap() => { + 'title': title, + 'text': text, + 'email': email, + 'bugLabel': bugLabel!.item2, + 'faculties': faculties + }; +} diff --git a/uni/lib/model/entities/calendar_event.dart b/uni/lib/model/entities/calendar_event.dart index cf6e94ae8..eebe459cd 100644 --- a/uni/lib/model/entities/calendar_event.dart +++ b/uni/lib/model/entities/calendar_event.dart @@ -8,9 +8,6 @@ class CalendarEvent { /// Converts the event into a map Map toMap() { - return { - 'name': name, - 'date': date - }; + return {'name': name, 'date': date}; } } diff --git a/uni/lib/model/entities/exam.dart b/uni/lib/model/entities/exam.dart index 9dce8c199..eec51bce7 100644 --- a/uni/lib/model/entities/exam.dart +++ b/uni/lib/model/entities/exam.dart @@ -58,12 +58,12 @@ class Exam { 'Exames ao abrigo de estatutos especiais': 'EAE' }; - Exam(this.id, this.begin, this.end, this.subject, this.rooms, this.type, this.faculty); + Exam(this.id, this.begin, this.end, this.subject, this.rooms, this.type, + this.faculty); static List displayedTypes = types.keys.toList().sublist(0, 4); - - Exam.secConstructor( - this.id, this.subject, this.begin, this.end, String rooms, this.type,this.faculty) { + Exam.secConstructor(this.id, this.subject, this.begin, this.end, String rooms, + this.type, this.faculty) { this.rooms = rooms.split(','); } @@ -76,7 +76,7 @@ class Exam { 'end': DateFormat("yyyy-MM-dd HH:mm:ss").format(end), 'rooms': rooms.join(','), 'examType': type, - 'faculty':faculty + 'faculty': faculty }; } diff --git a/uni/lib/model/entities/lecture.dart b/uni/lib/model/entities/lecture.dart index a22a60274..166c72d80 100644 --- a/uni/lib/model/entities/lecture.dart +++ b/uni/lib/model/entities/lecture.dart @@ -1,76 +1,56 @@ import 'package:logger/logger.dart'; -import 'package:uni/model/entities/time_utilities.dart'; /// Stores information about a lecture. class Lecture { String subject; - String startTime; - String endTime; String typeClass; String room; String teacher; String classNumber; - int day; + DateTime startTime; + DateTime endTime; int blocks; - int startTimeSeconds; int occurrId; /// Creates an instance of the class [Lecture]. Lecture( this.subject, this.typeClass, - this.day, + this.startTime, + this.endTime, this.blocks, this.room, this.teacher, this.classNumber, - int startTimeHours, - int startTimeMinutes, - int endTimeHours, - int endTimeMinutes, - this.occurrId) - : startTime = '${startTimeHours.toString().padLeft(2, '0')}h' - '${startTimeMinutes.toString().padLeft(2, '0')}', - endTime = '${endTimeHours.toString().padLeft(2, '0')}h' - '${endTimeMinutes.toString().padLeft(2, '0')}', - startTimeSeconds = 0; + this.occurrId); factory Lecture.fromApi( String subject, String typeClass, - int day, - int startTimeSeconds, + DateTime startTime, int blocks, String room, String teacher, String classNumber, int occurrId) { - final startTimeHours = (startTimeSeconds ~/ 3600); - final startTimeMinutes = ((startTimeSeconds % 3600) ~/ 60); - final endTimeSeconds = 60 * 30 * blocks + startTimeSeconds; - final endTimeHours = (endTimeSeconds ~/ 3600); - final endTimeMinutes = ((endTimeSeconds % 3600) ~/ 60); + final endTime = startTime.add(Duration(seconds:60 * 30 * blocks)); final lecture = Lecture( subject, typeClass, - day, + startTime, + endTime, blocks, room, teacher, classNumber, - startTimeHours, - startTimeMinutes, - endTimeHours, - endTimeMinutes, occurrId); - lecture.startTimeSeconds = startTimeSeconds; return lecture; } factory Lecture.fromHtml( String subject, String typeClass, - int day, + DateTime day, String startTime, int blocks, String room, @@ -85,15 +65,12 @@ class Lecture { return Lecture( subject, typeClass, - day, + day.add(Duration(hours: startTimeHours, minutes: startTimeMinutes)), + day.add(Duration(hours: startTimeMinutes+endTimeHours, minutes: startTimeMinutes+endTimeMinutes)), blocks, room, teacher, classNumber, - startTimeHours, - startTimeMinutes, - endTimeHours, - endTimeMinutes, occurrId); } @@ -102,8 +79,7 @@ class Lecture { return Lecture.fromApi( lec.subject, lec.typeClass, - lec.day, - lec.startTimeSeconds, + lec.startTime, lec.blocks, lec.room, lec.teacher, @@ -113,8 +89,7 @@ class Lecture { /// Clones a lecture from the html. static Lecture cloneHtml(Lecture lec) { - return Lecture.fromHtml(lec.subject, lec.typeClass, lec.day, lec.startTime, - lec.blocks, lec.room, lec.teacher, lec.classNumber, lec.occurrId); + return Lecture.clone(lec); } /// Converts this lecture to a map. @@ -122,8 +97,7 @@ class Lecture { return { 'subject': subject, 'typeClass': typeClass, - 'day': day, - 'startTime': startTime, + 'startDateTime': startTime.toIso8601String(), 'blocks': blocks, 'room': room, 'teacher': teacher, @@ -134,23 +108,22 @@ class Lecture { /// Prints the data in this lecture to the [Logger] with an INFO level. printLecture() { - Logger().i('$subject $typeClass'); - Logger().i('${TimeString.getWeekdaysStrings()[day]} $startTime $endTime $blocks blocos'); - Logger().i('$room $teacher\n'); + Logger().i(toString()); + } + + @override + String toString() { + return "$subject $typeClass\n$startTime $endTime $blocks blocos\n $room $teacher\n"; } /// Compares the date and time of two lectures. int compare(Lecture other) { - if (day == other.day) { - return startTime.compareTo(other.startTime); - } else { - return day.compareTo(other.day); - } + return startTime.compareTo(other.startTime); } @override int get hashCode => Object.hash(subject, startTime, endTime, typeClass, room, - teacher, day, blocks, startTimeSeconds, occurrId); + teacher, startTime, blocks, occurrId); @override bool operator ==(other) => @@ -161,8 +134,6 @@ class Lecture { typeClass == other.typeClass && room == other.room && teacher == other.teacher && - day == other.day && blocks == other.blocks && - startTimeSeconds == other.startTimeSeconds && occurrId == other.occurrId; } diff --git a/uni/lib/model/entities/time_utilities.dart b/uni/lib/model/entities/time_utilities.dart index 9180392b8..d1d512d5a 100644 --- a/uni/lib/model/entities/time_utilities.dart +++ b/uni/lib/model/entities/time_utilities.dart @@ -1,9 +1,12 @@ +import 'package:flutter/material.dart'; + extension TimeString on DateTime { String toTimeHourMinString() { return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; } - static List getWeekdaysStrings({bool startMonday = true, bool includeWeekend = true}) { + static List getWeekdaysStrings( + {bool startMonday = true, bool includeWeekend = true}) { final List weekdays = [ 'Segunda-Feira', 'Terça-Feira', @@ -22,3 +25,13 @@ extension TimeString on DateTime { return includeWeekend ? weekdays : weekdays.sublist(0, 5); } } + +extension ClosestMonday on DateTime{ + DateTime getClosestMonday(){ + final DateTime day = DateUtils.dateOnly(this); + if(day.weekday >=1 && day.weekday <= 5){ + return day.subtract(Duration(days: day.weekday-1)); + } + return day.add(Duration(days: DateTime.daysPerWeek - day.weekday+1)); + } +} \ No newline at end of file diff --git a/uni/lib/utils/duration_string_formatter.dart b/uni/lib/utils/duration_string_formatter.dart new file mode 100644 index 000000000..91eef0fa7 --- /dev/null +++ b/uni/lib/utils/duration_string_formatter.dart @@ -0,0 +1,46 @@ +extension DurationStringFormatter on Duration{ + + static final formattingRegExp = RegExp('{}'); + + String toFormattedString(String singularPhrase, String pluralPhrase, {String term = "{}"}){ + if (!singularPhrase.contains(term) || !pluralPhrase.contains(term)) { + throw ArgumentError("singularPhrase or plurarPhrase don't have a string that can be formatted..."); + } + if(inSeconds == 1){ + return singularPhrase.replaceAll(formattingRegExp, "$inSeconds segundo"); + } + if(inSeconds < 60){ + return pluralPhrase.replaceAll(formattingRegExp, "$inSeconds segundos"); + } + if(inMinutes == 1){ + return singularPhrase.replaceAll(formattingRegExp, "$inMinutes minuto"); + } + if(inMinutes < 60){ + return pluralPhrase.replaceAll(formattingRegExp, "$inMinutes minutos"); + } + if(inHours == 1){ + return singularPhrase.replaceAll(formattingRegExp, "$inHours hora"); + } + if(inHours < 24){ + return pluralPhrase.replaceAll(formattingRegExp, "$inHours horas"); + } + if(inDays == 1){ + return singularPhrase.replaceAll(formattingRegExp, "$inDays dia"); + } + if(inDays <= 7){ + return pluralPhrase.replaceAll(formattingRegExp, "$inDays dias"); + + } + if((inDays / 7).floor() == 1){ + return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semana"); + } + if((inDays / 7).floor() > 1){ + return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semanas"); + } + if((inDays / 30).floor() == 1){ + return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} mês"); + } + return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} meses"); + + } +} \ No newline at end of file diff --git a/uni/lib/view/about/about.dart b/uni/lib/view/about/about.dart index 1bf311d1d..411a1901d 100644 --- a/uni/lib/view/about/about.dart +++ b/uni/lib/view/about/about.dart @@ -19,7 +19,8 @@ class AboutPageViewState extends GeneralPageViewState { children: [ SvgPicture.asset( 'assets/images/ni_logo.svg', - color: Theme.of(context).primaryColor, + colorFilter: + ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), width: queryData.size.height / 7, height: queryData.size.height / 7, ), diff --git a/uni/lib/view/bug_report/widgets/form.dart b/uni/lib/view/bug_report/widgets/form.dart index c4c21e8da..f63d5b9fb 100644 --- a/uni/lib/view/bug_report/widgets/form.dart +++ b/uni/lib/view/bug_report/widgets/form.dart @@ -61,6 +61,7 @@ class BugReportFormState extends State { bugDescriptions.forEach((int key, Tuple2 tup) => {bugList.add(DropdownMenuItem(value: key, child: Text(tup.item1)))}); } + @override Widget build(BuildContext context) { return Form( @@ -139,7 +140,7 @@ class BugReportFormState extends State { child: Text( '''Encontraste algum bug na aplicação?\nTens alguma ''' '''sugestão para a app?\nConta-nos para que possamos melhorar!''', - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center), ), ); @@ -155,7 +156,7 @@ class BugReportFormState extends State { children: [ Text( 'Tipo de ocorrência', - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left, ), Row(children: [ @@ -191,7 +192,7 @@ class BugReportFormState extends State { child: CheckboxListTile( title: Text( '''Consinto que esta informação seja revista pelo NIAEFEUP, podendo ser eliminada a meu pedido.''', - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left), value: _isConsentGiven, onChanged: (bool? newValue) { @@ -233,14 +234,15 @@ class BugReportFormState extends State { setState(() { _isButtonTapped = true; }); - final List faculties = await AppSharedPreferences.getUserFaculties(); + final List faculties = + await AppSharedPreferences.getUserFaculties(); final bugReport = BugReport( - titleController.text, - descriptionController.text, - emailController.text, - bugDescriptions[_selectedBug], - faculties - ).toMap(); + titleController.text, + descriptionController.text, + emailController.text, + bugDescriptions[_selectedBug], + faculties) + .toMap(); String toastMsg; bool status; try { @@ -262,13 +264,17 @@ class BugReportFormState extends State { if (mounted) { FocusScope.of(context).requestFocus(FocusNode()); - status ? ToastMessage.success(context, toastMsg) : ToastMessage.error(context, toastMsg); + status + ? ToastMessage.success(context, toastMsg) + : ToastMessage.error(context, toastMsg); setState(() { _isButtonTapped = false; }); } } - Future submitGitHubIssue(SentryId sentryEvent, Map bugReport) async { + + Future submitGitHubIssue( + SentryId sentryEvent, Map bugReport) async { final String description = '${bugReport['bugLabel']}\nFurther information on: $_sentryLink$sentryEvent'; final Map data = { @@ -276,7 +282,7 @@ class BugReportFormState extends State { 'body': description, 'labels': ['In-app bug report', bugReport['bugLabel']], }; - for (String faculty in bugReport['faculties']){ + for (String faculty in bugReport['faculties']) { data['labels'].add(faculty); } return http @@ -291,7 +297,7 @@ class BugReportFormState extends State { }); } - Future submitSentryEvent(Map bugReport) async { + Future submitSentryEvent(Map bugReport) async { final String description = bugReport['email'] == '' ? '${bugReport['text']} from ${bugReport['faculty']}' : '${bugReport['text']} from ${bugReport['faculty']}\nContact: ${bugReport['email']}'; diff --git a/uni/lib/view/bug_report/widgets/text_field.dart b/uni/lib/view/bug_report/widgets/text_field.dart index 6504609fb..ae021e20c 100644 --- a/uni/lib/view/bug_report/widgets/text_field.dart +++ b/uni/lib/view/bug_report/widgets/text_field.dart @@ -35,7 +35,7 @@ class FormTextField extends StatelessWidget { children: [ Text( description, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left, ), Row(children: [ @@ -52,9 +52,9 @@ class FormTextField extends StatelessWidget { decoration: InputDecoration( focusedBorder: const UnderlineInputBorder(), hintText: hintText, - hintStyle: Theme.of(context).textTheme.bodyText2, + hintStyle: Theme.of(context).textTheme.bodyMedium, labelText: labelText, - labelStyle: Theme.of(context).textTheme.bodyText2, + labelStyle: Theme.of(context).textTheme.bodyMedium, ), controller: controller, validator: (value) { diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart index ef2f81889..f0712c2c9 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart @@ -49,7 +49,7 @@ class BusStopRow extends StatelessWidget { return Text('Não há viagens planeadas de momento.', maxLines: 3, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.subtitle1); + style: Theme.of(context).textTheme.titleMedium); } Widget stopCodeRotatedContainer(context) { @@ -57,7 +57,7 @@ class BusStopRow extends StatelessWidget { padding: const EdgeInsets.only(left: 4.0), child: RotatedBox( quarterTurns: 3, - child: Text(stopCode, style: Theme.of(context).textTheme.subtitle1), + child: Text(stopCode, style: Theme.of(context).textTheme.titleMedium), ), ); } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index 9221f0cb9..c1ec2d134 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart @@ -28,6 +28,7 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { num = estimatedTime.minute; final String minute = (num >= 10 ? '$num' : '0$num'); - return Text('$hour:$minute', style: Theme.of(context).textTheme.subtitle1); + return Text('$hour:$minute', + style: Theme.of(context).textTheme.titleMedium); } } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart index 3c56378be..391146f72 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart @@ -21,14 +21,14 @@ class TripRow extends StatelessWidget { Text(trip.line, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.subtitle1), + style: Theme.of(context).textTheme.titleMedium), Text(trip.destination, - style: Theme.of(context).textTheme.subtitle1), + style: Theme.of(context).textTheme.titleMedium), ], ), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${trip.timeRemaining}\'', - style: Theme.of(context).textTheme.subtitle1), + style: Theme.of(context).textTheme.titleMedium), EstimatedArrivalTimeStamp( timeRemaining: trip.timeRemaining.toString()), ]) diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart index 88b58ae68..7402800a7 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart @@ -84,7 +84,7 @@ class BusStopSearch extends SearchDelegate { updateStopCallback); return AlertDialog( title: Text('Seleciona os autocarros dos quais queres informação:', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), content: SizedBox( height: 200.0, width: 100.0, @@ -93,7 +93,7 @@ class BusStopSearch extends SearchDelegate { actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyText2), + style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.pop(context)), ElevatedButton( child: const Text('Confirmar'), diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 57d1a78fd..4942cc005 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -52,13 +52,13 @@ class CalendarPageViewState extends GeneralPageViewState { child: Text(calendar[index].name, style: Theme.of(context) .textTheme - .headline6 + .titleLarge ?.copyWith(fontWeight: FontWeight.w500)), ), oppositeContentsBuilder: (context, index) => Padding( padding: const EdgeInsets.all(24.0), child: Text(calendar[index].date, - style: Theme.of(context).textTheme.subtitle1?.copyWith( + style: Theme.of(context).textTheme.titleMedium?.copyWith( fontStyle: FontStyle.italic, )), ), diff --git a/uni/lib/view/common_widgets/date_rectangle.dart b/uni/lib/view/common_widgets/date_rectangle.dart index 153d4b18c..b2e1a7d3d 100644 --- a/uni/lib/view/common_widgets/date_rectangle.dart +++ b/uni/lib/view/common_widgets/date_rectangle.dart @@ -15,7 +15,7 @@ class DateRectangle extends StatelessWidget { margin: const EdgeInsets.only(bottom: 10), alignment: Alignment.center, width: double.infinity, - child: Text(date, style: Theme.of(context).textTheme.subtitle2), + child: Text(date, style: Theme.of(context).textTheme.titleSmall), ); } } diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index be81dd354..dc92d9d04 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -37,7 +37,7 @@ abstract class GenericCard extends StatefulWidget { Text getInfoText(String text, BuildContext context) { return Text(text, textAlign: TextAlign.end, - style: Theme.of(context).textTheme.headline6!); + style: Theme.of(context).textTheme.titleLarge!); } showLastRefreshedTime(String? time, context) { @@ -53,7 +53,7 @@ abstract class GenericCard extends StatefulWidget { return Container( alignment: Alignment.center, child: Text('última atualização às ${parsedTime.toTimeHourMinString()}', - style: Theme.of(context).textTheme.caption)); + style: Theme.of(context).textTheme.bodySmall)); } } @@ -105,10 +105,12 @@ class GenericCardState extends State { margin: const EdgeInsets.only(top: 15, bottom: 10), child: Text(widget.getTitle(), style: (widget.smallTitle - ? Theme.of(context).textTheme.headline6! + ? Theme.of(context) + .textTheme + .titleLarge! : Theme.of(context) .textTheme - .headline5!) + .headlineSmall!) .copyWith( color: Theme.of(context).primaryColor)), )), diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index 9d88e714c..f7429d9dd 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -29,7 +29,7 @@ class GenericExpansionCardState extends State { title: Text(widget.getTitle(), style: Theme.of(context) .textTheme - .headline5 + .headlineSmall ?.apply(color: Theme.of(context).primaryColor)), elevation: 0, children: [ diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 23617e16e..4b5138ce2 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -53,7 +53,7 @@ class _LastUpdateTimeStampState extends State { children: [ Text( 'Atualizado há $elapsedTimeMinutes minuto${elapsedTimeMinutes != 1 ? 's' : ''}', - style: Theme.of(context).textTheme.subtitle2) + style: Theme.of(context).textTheme.titleSmall) ]); } } diff --git a/uni/lib/view/common_widgets/page_title.dart b/uni/lib/view/common_widgets/page_title.dart index 9977d4fd6..7cebe9f0a 100644 --- a/uni/lib/view/common_widgets/page_title.dart +++ b/uni/lib/view/common_widgets/page_title.dart @@ -14,8 +14,8 @@ class PageTitle extends StatelessWidget { Widget build(BuildContext context) { final Widget title = Text( name, - style: Theme.of(context).textTheme.headline4?.copyWith( - color: Theme.of(context).primaryTextTheme.headline4?.color), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).primaryTextTheme.headlineMedium?.color), ); return Container( padding: pad ? const EdgeInsets.fromLTRB(20, 20, 20, 10) : null, 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 152923cb6..ad8f9f20e 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -107,7 +107,8 @@ abstract class GeneralPageViewState extends State { } }, child: SvgPicture.asset( - color: Theme.of(context).primaryColor, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, BlendMode.srcIn), 'assets/images/logo_dark.svg', height: queryData.size.height / 25, ), diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart index ac5e65da2..cacfd953c 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart @@ -77,7 +77,7 @@ class AppNavigationDrawerState extends State { child: Text(logOutText, style: Theme.of(context) .textTheme - .headline6! + .titleLarge! .copyWith(color: Theme.of(context).primaryColor)), ), ); diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index 8ba72d406..f5f0b2d1d 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -50,13 +50,15 @@ class RequestDependentWidgetBuilder extends StatelessWidget { ? contentGenerator(content, context) : onNullContent; } - if (contentLoadingWidget != null){ + if (contentLoadingWidget != null) { return contentChecker ? contentGenerator(content, context) - : Center(child: Shimmer.fromColors( - baseColor: Theme.of(context).highlightColor, - highlightColor: Theme.of(context).colorScheme.onPrimary, - child: contentLoadingWidget!)); + : Center( + child: Shimmer.fromColors( + baseColor: Theme.of(context).highlightColor, + highlightColor: + Theme.of(context).colorScheme.onPrimary, + child: contentLoadingWidget!)); } return contentChecker ? contentGenerator(content, context) @@ -80,7 +82,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { return Center( heightFactor: 3, child: Text('Sem ligação à internet', - style: Theme.of(context).textTheme.subtitle1)); + style: Theme.of(context).textTheme.titleMedium)); } } return Column(children: [ @@ -88,7 +90,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { padding: const EdgeInsets.only(top: 15, bottom: 10), child: Center( child: Text('Aconteceu um erro ao carregar os dados', - style: Theme.of(context).textTheme.subtitle1))), + style: Theme.of(context).textTheme.titleMedium))), OutlinedButton( onPressed: () => Navigator.pushNamed(context, '/${DrawerItem.navBugReport.title}'), diff --git a/uni/lib/view/common_widgets/toast_message.dart b/uni/lib/view/common_widgets/toast_message.dart index 0a1170e47..65854de38 100644 --- a/uni/lib/view/common_widgets/toast_message.dart +++ b/uni/lib/view/common_widgets/toast_message.dart @@ -75,9 +75,9 @@ class ToastMessage { barrierDismissible: false, barrierColor: Colors.white.withOpacity(0), context: context, - builder: (_) { + builder: (toastContext) { Future.delayed(const Duration(milliseconds: 2000), () { - Navigator.of(context).pop(); + Navigator.of(toastContext).pop(); }); return mToast; }); diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index d45b605ce..19a2b94e4 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -87,7 +87,7 @@ class CourseUnitsPageViewState onNullContent: Center( heightFactor: 10, child: Text('Não existem cadeiras para apresentar', - style: Theme.of(context).textTheme.headline6), + style: Theme.of(context).textTheme.titleLarge), )) ]); } @@ -142,7 +142,7 @@ class CourseUnitsPageViewState return Center( heightFactor: 10, child: Text('Sem cadeiras no período selecionado', - style: Theme.of(context).textTheme.headline6)); + style: Theme.of(context).textTheme.titleLarge)); } return Expanded( child: Container( diff --git a/uni/lib/view/exams/widgets/day_title.dart b/uni/lib/view/exams/widgets/day_title.dart index fc11f0236..0dc07515c 100644 --- a/uni/lib/view/exams/widgets/day_title.dart +++ b/uni/lib/view/exams/widgets/day_title.dart @@ -18,7 +18,7 @@ class DayTitle extends StatelessWidget { alignment: Alignment.center, child: Text( '$weekDay, $day de $month', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), ); } diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 53db40292..766092afa 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -19,11 +19,11 @@ class ExamFilterFormState extends State { Widget build(BuildContext context) { return AlertDialog( title: Text('Definições Filtro de Exames', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), actions: [ TextButton( child: - Text('Cancelar', style: Theme.of(context).textTheme.bodyText2), + Text('Cancelar', style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.pop(context)), ElevatedButton( child: const Text('Confirmar'), @@ -43,8 +43,7 @@ class ExamFilterFormState extends State { Widget getExamCheckboxes( Map filteredExams, BuildContext context) { - filteredExams - .removeWhere((key, value) => !Exam.types.containsKey(key)); + filteredExams.removeWhere((key, value) => !Exam.types.containsKey(key)); return ListView( children: List.generate(filteredExams.length, (i) { final String key = filteredExams.keys.elementAt(i); diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index fadfe586c..21fbc1347 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -30,7 +30,8 @@ class ExamRow extends StatefulWidget { class _ExamRowState extends State { @override Widget build(BuildContext context) { - final isHidden = Provider.of(context).hiddenExams.contains(widget.exam.id); + final isHidden = + Provider.of(context).hiddenExams.contains(widget.exam.id); final roomsKey = '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.beginTime}-${widget.exam.endTime}'; return Center( @@ -52,8 +53,8 @@ class _ExamRowState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ ExamTime( - begin: widget.exam.beginTime, - end: widget.exam.endTime) + begin: widget.exam.beginTime, + ) ]), ExamTitle( subject: widget.exam.subject, @@ -105,7 +106,7 @@ class _ExamRowState extends State { List roomsList(BuildContext context, List rooms) { return rooms .map((room) => - Text(room.trim(), style: Theme.of(context).textTheme.bodyText2)) + Text(room.trim(), style: Theme.of(context).textTheme.bodyMedium)) .toList(); } diff --git a/uni/lib/view/exams/widgets/exam_time.dart b/uni/lib/view/exams/widgets/exam_time.dart index 443441e84..1c0615690 100644 --- a/uni/lib/view/exams/widgets/exam_time.dart +++ b/uni/lib/view/exams/widgets/exam_time.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; class ExamTime extends StatelessWidget { final String begin; - final String end; - const ExamTime({Key? key, required this.begin, required this.end}) + const ExamTime({Key? key, required this.begin}) : super(key: key); @override @@ -13,8 +12,7 @@ class ExamTime extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max, children: [ - Text(begin, style: Theme.of(context).textTheme.bodyText2), - Text(end, style: Theme.of(context).textTheme.bodyText2), + Text(begin, style: Theme.of(context).textTheme.bodyMedium), ], ); } diff --git a/uni/lib/view/exams/widgets/exam_title.dart b/uni/lib/view/exams/widgets/exam_title.dart index 8fb7c91eb..743a0a952 100644 --- a/uni/lib/view/exams/widgets/exam_title.dart +++ b/uni/lib/view/exams/widgets/exam_title.dart @@ -20,9 +20,12 @@ class ExamTitle extends StatelessWidget { Widget createTopRectangle(context) { final Text typeWidget = Text(type != null ? ' ($type) ' : '', - style: Theme.of(context).textTheme.bodyText2); - final Text subjectWidget = - Text(subject, style: Theme.of(context).textTheme.headline5?.apply(color: Theme.of(context).colorScheme.tertiary)); + style: Theme.of(context).textTheme.bodyMedium); + final Text subjectWidget = Text(subject, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.apply(color: Theme.of(context).colorScheme.tertiary)); return Row( children: (reverseOrder diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 685d31101..ff567736a 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -50,7 +50,7 @@ Widget getCardContent(BuildContext context, Map stopData, b Text('Configura os teus autocarros', maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.subtitle2!.apply()), + style: Theme.of(context).textTheme.titleSmall!.apply()), IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.push( @@ -77,7 +77,7 @@ Widget getCardContent(BuildContext context, Map stopData, b Container( padding: const EdgeInsets.all(8.0), child: Text('Não foi possível obter informação', - style: Theme.of(context).textTheme.subtitle1)) + style: Theme.of(context).textTheme.titleMedium)) ]); } } @@ -88,7 +88,7 @@ Widget getCardTitle(context) { children: [ const Icon(Icons.directions_bus), // color lightgrey Text('STCP - Próximas Viagens', - style: Theme.of(context).textTheme.subtitle1), + style: Theme.of(context).textTheme.titleMedium), ], ); } diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index aa7d4f268..331a48e26 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -46,7 +46,7 @@ class ExamCard extends GenericCard { contentChecker: exams.isNotEmpty, onNullContent: Center( child: Text('Não existem exames para apresentar', - style: Theme.of(context).textTheme.headline6), + style: Theme.of(context).textTheme.titleLarge), ), contentLoadingWidget: const ExamCardShimmer().build(context), ); @@ -106,7 +106,7 @@ class ExamCard extends GenericCard { return Container( margin: const EdgeInsets.only(top: 8), child: RowContainer( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: Container( padding: const EdgeInsets.all(11), child: Row( @@ -116,7 +116,7 @@ class ExamCard extends GenericCard { children: [ Text( '${exam.begin.day} de ${exam.month}', - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context).textTheme.bodyLarge, ), ExamTitle( subject: exam.subject, type: exam.type, reverseOrder: true) diff --git a/uni/lib/view/home/widgets/exam_card_shimmer.dart b/uni/lib/view/home/widgets/exam_card_shimmer.dart index 8f85f59e4..55cb29ee3 100644 --- a/uni/lib/view/home/widgets/exam_card_shimmer.dart +++ b/uni/lib/view/home/widgets/exam_card_shimmer.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -class ExamCardShimmer extends StatelessWidget{ - const ExamCardShimmer({Key? key}): super(key: key); - +class ExamCardShimmer extends StatelessWidget { + const ExamCardShimmer({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return Center( - child: Container( + child: Container( padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), margin: const EdgeInsets.only(top: 8.0), child: Column( @@ -24,63 +24,80 @@ class ExamCardShimmer extends StatelessWidget{ crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox(height: 2.5,), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - - ], - ) + mainAxisAlignment: + MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + height: 2.5, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + ], + ) ]), - Container(height: 30, width: 100, color: Colors.black,), //UC section - Container(height: 40, width: 40, color: Colors.black,), //Calender add section + Container( + height: 30, + width: 100, + color: Colors.black, + ), //UC section + Container( + height: 40, + width: 40, + color: Colors.black, + ), //Calender add section ], )), - const SizedBox(height: 10,), - Row( //Exam room section - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox(width: 10,), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox(width: 10,), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox(width: 10,), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) + const SizedBox( + height: 10, + ), + Row( + //Exam room section + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + ], + ) ], ))); } - - - } diff --git a/uni/lib/view/home/widgets/exit_app_dialog.dart b/uni/lib/view/home/widgets/exit_app_dialog.dart index 057575fd1..d078f05ac 100644 --- a/uni/lib/view/home/widgets/exit_app_dialog.dart +++ b/uni/lib/view/home/widgets/exit_app_dialog.dart @@ -17,7 +17,7 @@ class BackButtonExitWrapper extends StatelessWidget { context: context, builder: (context) => AlertDialog( title: Text('Tens a certeza de que pretendes sair?', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(false), diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index ee2fa9c63..857a55140 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -85,7 +85,7 @@ class MainCardsList extends StatelessWidget { return AlertDialog( title: Text( 'Escolhe um widget para adicionares à tua área pessoal:', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), content: SizedBox( height: 200.0, width: 100.0, @@ -94,7 +94,7 @@ class MainCardsList extends StatelessWidget { actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyText2), + style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.pop(context)) ]); }), //Add FAB functionality here @@ -148,7 +148,7 @@ class MainCardsList extends StatelessWidget { .setHomePageEditingMode(!editingModeProvider.isEditing), child: Text( editingModeProvider.isEditing ? 'Concluir Edição' : 'Editar', - style: Theme.of(context).textTheme.caption)) + style: Theme.of(context).textTheme.bodySmall)) ]), ); } diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 69255adfa..4eeb035d6 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -32,7 +32,7 @@ class RestaurantCard extends GenericCard { contentChecker: restaurantProvider.restaurants.isNotEmpty, onNullContent: Center( child: Text('Não existem cantinas para apresentar', - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center)))); } diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index 2ba31b108..a17fdc2d0 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -10,7 +10,6 @@ import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import 'package:uni/view/home/widgets/schedule_card_shimmer.dart'; import 'package:uni/utils/drawer_items.dart'; - class ScheduleCard extends GenericCard { ScheduleCard({Key? key}) : super(key: key); @@ -33,7 +32,7 @@ class ScheduleCard extends GenericCard { contentChecker: lectureProvider.lectures.isNotEmpty, onNullContent: Center( child: Text('Não existem aulas para apresentar', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center)), contentLoadingWidget: const ScheduleCardShimmer().build(context)) ); @@ -49,41 +48,27 @@ class ScheduleCard extends GenericCard { } List getScheduleRows(BuildContext context, List lectures) { - if (lectures.length >= 2) { - // In order to display lectures of the next week - final Lecture lecturefirstCycle = Lecture.cloneHtml(lectures[0]); - lecturefirstCycle.day += 7; - final Lecture lecturesecondCycle = Lecture.cloneHtml(lectures[1]); - lecturesecondCycle.day += 7; - lectures.add(lecturefirstCycle); - lectures.add(lecturesecondCycle); - } final List rows = []; final now = DateTime.now(); var added = 0; // Lectures added to widget - var lastDayAdded = 0; // Day of last added lecture - final stringTimeNow = (now.weekday - 1).toString().padLeft(2, '0') + - now.toTimeHourMinString(); // String with current time within the week + DateTime lastAddedLectureDate = DateTime.now(); // Day of last added lecture for (int i = 0; added < 2 && i < lectures.length; i++) { - final stringEndTimeLecture = lectures[i].day.toString().padLeft(2, '0') + - lectures[i].endTime; // String with end time of lecture - - if (stringTimeNow.compareTo(stringEndTimeLecture) < 0) { - if (now.weekday - 1 != lectures[i].day && - lastDayAdded < lectures[i].day) { - rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[i].day % 7])); + if (now.compareTo(lectures[i].endTime) < 0) { + if (lastAddedLectureDate.weekday != lectures[i].startTime.weekday && + lastAddedLectureDate.compareTo(lectures[i].startTime) <= 0) { + rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[(lectures[i].startTime.weekday-1) % 7])); } rows.add(createRowFromLecture(context, lectures[i])); - lastDayAdded = lectures[i].day; + lastAddedLectureDate = lectures[i].startTime; added++; } } if (rows.isEmpty) { - rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[0].day % 7])); + rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[0].startTime.weekday % 7])); rows.add(createRowFromLecture(context, lectures[0])); } return rows; diff --git a/uni/lib/view/home/widgets/schedule_card_shimmer.dart b/uni/lib/view/home/widgets/schedule_card_shimmer.dart index c4e06838b..506ac0621 100644 --- a/uni/lib/view/home/widgets/schedule_card_shimmer.dart +++ b/uni/lib/view/home/widgets/schedule_card_shimmer.dart @@ -1,74 +1,94 @@ import 'package:flutter/material.dart'; - -class ScheduleCardShimmer extends StatelessWidget{ +class ScheduleCardShimmer extends StatelessWidget { const ScheduleCardShimmer({Key? key}) : super(key: key); - - Widget _getSingleScheduleWidget(BuildContext context){ + + Widget _getSingleScheduleWidget(BuildContext context) { return Center( + child: Container( + padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), + margin: const EdgeInsets.only(top: 8.0), child: Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), - child: Container( - margin: const EdgeInsets.only(top: 8, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox(height: 2.5,), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - - ], - ) - ]), + margin: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container(height: 25, width: 100, color: Colors.black,), //UC section - const SizedBox(height: 10,), - Container(height: 15, width: 150, color: Colors.black,), //UC section - + //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + height: 2.5, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), ], - ), - Container(height: 15, width: 40, color: Colors.black,), //Room section - ], - )), - )); + ) + ]), + Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 25, + width: 100, + color: Colors.black, + ), //UC section + const SizedBox( + height: 10, + ), + Container( + height: 15, + width: 150, + color: Colors.black, + ), //UC section + ], + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), //Room section + ], + )), + )); } @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container(height: 15, width: 80, color: Colors.black,), //Day of the week - const SizedBox(height: 10,), + Container( + height: 15, + width: 80, + color: Colors.black, + ), //Day of the week + const SizedBox( + height: 10, + ), _getSingleScheduleWidget(context), _getSingleScheduleWidget(context), ], ); } -} \ No newline at end of file +} diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 228110583..0f4f1d7a1 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -94,13 +94,13 @@ class LibraryPage extends StatelessWidget { child: Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text('Piso ${floor.number}', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), Text('${floor.percentage}%', - style: Theme.of(context).textTheme.headline6), + style: Theme.of(context).textTheme.titleLarge), Text('${floor.occupation}/${floor.capacity}', style: Theme.of(context) .textTheme - .headline6 + .titleLarge ?.copyWith(color: Theme.of(context).colorScheme.background)), LinearPercentIndicator( lineHeight: 7.0, diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index 966d9c71d..bcaa96d43 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -40,7 +40,7 @@ class LibraryOccupationCard extends GenericCard { if (occupation == null || occupation.capacity == 0) { return Center( child: Text('Não existem dados para apresentar', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center)); } return Padding( @@ -52,13 +52,13 @@ class LibraryOccupationCard extends GenericCard { center: Text('${occupation.percentage}%', style: Theme.of(context) .textTheme - .headline2 + .displayMedium ?.copyWith(fontSize: 23, fontWeight: FontWeight.w500)), footer: Column( children: [ const Padding(padding: EdgeInsets.fromLTRB(0, 5.0, 0, 0)), Text('${occupation.occupation}/${occupation.capacity}', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), ], ), circularStrokeCap: CircularStrokeCap.square, diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index 5c49691b6..b9ce8722c 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -26,11 +26,6 @@ class LocationsPageState extends GeneralPageViewState super.initState(); } - @override - void dispose() { - super.dispose(); - } - @override Widget getBody(BuildContext context) { return Consumer( diff --git a/uni/lib/view/locations/widgets/faculty_maps.dart b/uni/lib/view/locations/widgets/faculty_maps.dart index 5d6287a48..7d113e654 100644 --- a/uni/lib/view/locations/widgets/faculty_maps.dart +++ b/uni/lib/view/locations/widgets/faculty_maps.dart @@ -22,8 +22,8 @@ class FacultyMaps { ); } - static getFontColor(BuildContext context){ - return Theme.of(context).brightness == Brightness.light + static getFontColor(BuildContext context) { + return Theme.of(context).brightness == Brightness.light ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.tertiary; } diff --git a/uni/lib/view/locations/widgets/floorless_marker_popup.dart b/uni/lib/view/locations/widgets/floorless_marker_popup.dart index 419787ac0..c7129ab87 100644 --- a/uni/lib/view/locations/widgets/floorless_marker_popup.dart +++ b/uni/lib/view/locations/widgets/floorless_marker_popup.dart @@ -15,7 +15,7 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { final List locations = locationGroup.floors.values.expand((x) => x).toList(); return Card( - color: Theme.of(context).backgroundColor.withOpacity(0.8), + color: Theme.of(context).colorScheme.background.withOpacity(0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), diff --git a/uni/lib/view/locations/widgets/icons.dart b/uni/lib/view/locations/widgets/icons.dart index b1958ac3d..7e3d41972 100644 --- a/uni/lib/view/locations/widgets/icons.dart +++ b/uni/lib/view/locations/widgets/icons.dart @@ -11,7 +11,7 @@ /// fonts: /// - asset: fonts/LocationIcons.ttf /// -/// +/// /// import 'package:flutter/widgets.dart'; @@ -22,13 +22,13 @@ class LocationIcons { static const String? _kFontPkg = null; static const IconData bookOpenBlankVariant = - IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData bottleSodaClassic = - IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData cashMultiple = - IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData coffee = - IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData printer = - IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index 3e281df5b..6eb951323 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -59,12 +59,10 @@ class LocationsMap extends StatelessWidget { ) ], children: [ - TileLayerWidget( - options: TileLayerOptions( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - tileProvider: CachedTileProvider(), - ), + TileLayer( + urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: const ['a', 'b', 'c'], + tileProvider: CachedTileProvider(), ), PopupMarkerLayerWidget( options: PopupMarkerLayerOptions( @@ -96,7 +94,7 @@ class CachedTileProvider extends TileProvider { CachedTileProvider(); @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { + ImageProvider getImage(Coords coords, TileLayer options) { return CachedNetworkImageProvider( getTileUrl(coords, options), ); diff --git a/uni/lib/view/locations/widgets/marker.dart b/uni/lib/view/locations/widgets/marker.dart index 677efed26..d3cca2d33 100644 --- a/uni/lib/view/locations/widgets/marker.dart +++ b/uni/lib/view/locations/widgets/marker.dart @@ -17,7 +17,7 @@ class LocationMarker extends Marker { point: latlng, builder: (BuildContext ctx) => Container( decoration: BoxDecoration( - color: Theme.of(ctx).backgroundColor, + color: Theme.of(ctx).colorScheme.background, border: Border.all( color: Theme.of(ctx).colorScheme.primary, ), diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 0af9b1eb2..87b653fd8 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -13,10 +13,7 @@ class LocationMarkerPopup extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: Theme - .of(context) - .backgroundColor - .withOpacity(0.8), + color: Theme.of(context).colorScheme.background.withOpacity(0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), @@ -26,8 +23,8 @@ class LocationMarkerPopup extends StatelessWidget { direction: Axis.vertical, spacing: 8, children: (showId - ? [Text(locationGroup.id.toString())] - : []) + + ? [Text(locationGroup.id.toString())] + : []) + buildFloors(context), )), ); @@ -36,7 +33,7 @@ class LocationMarkerPopup extends StatelessWidget { List buildFloors(BuildContext context) { //Sort by floor final List>> entries = - locationGroup.floors.entries.toList(); + locationGroup.floors.entries.toList(); entries.sort((current, next) => -current.key.compareTo(next.key)); return entries.map((entry) { @@ -47,28 +44,28 @@ class LocationMarkerPopup extends StatelessWidget { }).toList(); } - List buildFloor(BuildContext context, floor, - List locations) { + List buildFloor( + BuildContext context, floor, List locations) { final Color fontColor = FacultyMaps.getFontColor(context); final String floorString = - 0 <= floor && floor <= 9 //To maintain layout of popup - ? ' $floor' - : '$floor'; + 0 <= floor && floor <= 9 //To maintain layout of popup + ? ' $floor' + : '$floor'; final Widget floorCol = Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: Text( - 'Andar $floorString', style: TextStyle(color: fontColor))) + child: + Text('Andar $floorString', style: TextStyle(color: fontColor))) ], ); final Widget locationsColumn = Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), decoration: - BoxDecoration(border: Border(left: BorderSide(color: fontColor))), + BoxDecoration(border: Border(left: BorderSide(color: fontColor))), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -77,17 +74,16 @@ class LocationMarkerPopup extends StatelessWidget { return [floorCol, locationsColumn]; } - List buildLocations(BuildContext context, List locations, - Color color) { + List buildLocations( + BuildContext context, List locations, Color color) { return locations - .map((location) => - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(location.description(), - textAlign: TextAlign.left, style: TextStyle(color: color)) - ], - )) + .map((location) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(location.description(), + textAlign: TextAlign.left, style: TextStyle(color: color)) + ], + )) .toList(); } } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index c74b1b73a..571e6b2a4 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -164,7 +164,8 @@ class LoginPageViewState extends State { width: 100.0, child: SvgPicture.asset( 'assets/images/logo_dark.svg', - color: Colors.white, + colorFilter: + const ColorFilter.mode(Colors.white, BlendMode.srcIn), )), ])); } @@ -199,7 +200,7 @@ class LoginPageViewState extends State { return InkWell( child: Center( child: Text("Esqueceu a palavra-passe?", - style: Theme.of(context).textTheme.bodyText1!.copyWith( + style: Theme.of(context).textTheme.bodyLarge!.copyWith( decoration: TextDecoration.underline, color: Colors.white))), onTap: () => launchUrl(Uri.parse("https://self-id.up.pt/reset"))); diff --git a/uni/lib/view/login/widgets/faculties_selection_form.dart b/uni/lib/view/login/widgets/faculties_selection_form.dart index 6949317c8..7ce700eb8 100644 --- a/uni/lib/view/login/widgets/faculties_selection_form.dart +++ b/uni/lib/view/login/widgets/faculties_selection_form.dart @@ -33,7 +33,8 @@ class _FacultiesSelectionFormState extends State { child: const Text('Cancelar', style: TextStyle(color: Colors.white))), ElevatedButton( style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, backgroundColor: Colors.white), + foregroundColor: Theme.of(context).primaryColor, + backgroundColor: Colors.white), onPressed: () { if (widget.selectedFaculties.isEmpty) { ToastMessage.warning( diff --git a/uni/lib/view/navigation_service.dart b/uni/lib/view/navigation_service.dart index 8a163b060..89bf885b2 100644 --- a/uni/lib/view/navigation_service.dart +++ b/uni/lib/view/navigation_service.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:uni/utils/drawer_items.dart'; - /// Manages the navigation logic class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); static logout() { - navigatorKey.currentState! - .pushNamedAndRemoveUntil('/${DrawerItem.navLogOut.title}', (_) => false); + navigatorKey.currentState!.pushNamedAndRemoveUntil( + '/${DrawerItem.navLogOut.title}', (_) => false); } } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index e690f0baf..47a6363f5 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -28,7 +28,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 20.0, bottom: 8.0, left: 20.0), child: Text('Saldo: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: const EdgeInsets.only( @@ -40,7 +40,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 8.0, bottom: 20.0, left: 20.0), child: Text('Data limite próxima prestação: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: const EdgeInsets.only( @@ -52,7 +52,7 @@ class AccountInfoCard extends GenericCard { margin: const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), child: Text("Notificar próxima data limite: ", - style: Theme.of(context).textTheme.subtitle2) + style: Theme.of(context).textTheme.titleSmall) ), Container( margin: diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index 4c2b14476..86fb0b052 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -18,7 +18,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 20.0), child: Text('Ano curricular atual: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: @@ -30,7 +30,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Estado atual: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: @@ -42,7 +42,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Ano da primeira inscrição: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: @@ -57,7 +57,7 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Faculdade: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: @@ -68,8 +68,8 @@ class CourseInfoCard extends GenericCard { TableRow(children: [ Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: - Text('Média: ', style: Theme.of(context).textTheme.subtitle2), + child: Text('Média: ', + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: @@ -83,7 +83,7 @@ class CourseInfoCard extends GenericCard { margin: const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), child: Text('ECTs realizados: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: diff --git a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart index 612603931..c88dd2acb 100644 --- a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart +++ b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart @@ -34,7 +34,7 @@ Future addMoneyDialog(BuildContext context) async { child: Text( 'Os dados da referência gerada aparecerão no Sigarra, conta corrente. \nPerfil > Conta Corrente', textAlign: TextAlign.start, - style: Theme.of(context).textTheme.subtitle2)), + style: Theme.of(context).textTheme.titleSmall)), Row(children: [ IconButton( icon: const Icon(Icons.indeterminate_check_box), @@ -85,11 +85,11 @@ Future addMoneyDialog(BuildContext context) async { ], )), title: Text('Adicionar quota', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), actions: [ TextButton( child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyText2), + style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.pop(context)), ElevatedButton( onPressed: () => generateReference(context, value), diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index c30e87e37..eb0155295 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -31,13 +31,13 @@ class PrintInfoCard extends GenericCard { margin: const EdgeInsets.only( top: 20.0, bottom: 20.0, left: 20.0), child: Text('Valor disponível: ', - style: Theme.of(context).textTheme.subtitle2), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: const EdgeInsets.only(right: 15.0), child: Text(profile.printBalance, textAlign: TextAlign.end, - style: Theme.of(context).textTheme.headline6)), + style: Theme.of(context).textTheme.titleLarge)), Container( margin: const EdgeInsets.only(right: 5.0), height: 30, diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 3af637025..8d5144c51 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -31,8 +31,7 @@ class _CanteenPageState extends GeneralPageViewState final int weekDay = DateTime.now().weekday; super.initState(); tabController = TabController(vsync: this, length: DayOfWeek.values.length); - final offset = (weekDay > 5) ? 0 : (weekDay - 1) % DayOfWeek.values.length; - tabController.animateTo((tabController.index + offset)); + tabController.animateTo((tabController.index + (weekDay-1))); scrollViewController = ScrollController(); } @@ -65,7 +64,8 @@ class _CanteenPageState extends GeneralPageViewState contentGenerator: createTabViewBuilder, content: restaurants, contentChecker: restaurants.isNotEmpty, - onNullContent: const Center(child: Text('Não há refeições disponíveis.'))) + onNullContent: + const Center(child: Text('Não há refeições disponíveis.'))) ]); } @@ -92,7 +92,7 @@ class _CanteenPageState extends GeneralPageViewState for (var i = 0; i < DayOfWeek.values.length; i++) { tabs.add(Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: Tab(key: Key('cantine-page-tab-$i'), text: toString(DayOfWeek.values[i])), )); } @@ -101,7 +101,8 @@ class _CanteenPageState extends GeneralPageViewState } Widget createRestaurant(context, Restaurant restaurant, DayOfWeek dayOfWeek) { - return RestaurantPageCard(restaurant.name, createRestaurantByDay(context, restaurant, dayOfWeek)); + return RestaurantPageCard( + restaurant.name, createRestaurantByDay(context, restaurant, dayOfWeek)); } List createRestaurantRows(List meals, BuildContext context) { @@ -115,25 +116,23 @@ class _CanteenPageState extends GeneralPageViewState final List meals = restaurant.getMealsOfDay(day); if (meals.isEmpty) { return Container( - margin: - const EdgeInsets.only(top: 10, bottom: 5), + margin: const EdgeInsets.only(top: 10, bottom: 5), key: Key('cantine-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, - children: - const [Center (child: Text("Não há informação disponível sobre refeições")),], - ) - ); + children: const [ + Center( + child: Text("Não há informação disponível sobre refeições")), + ], + )); } else { return Container( - margin: - const EdgeInsets.only(top: 5, bottom: 5), - key: Key('cantine-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: createRestaurantRows(meals, context), - ) - ); + margin: const EdgeInsets.only(top: 5, bottom: 5), + key: Key('cantine-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: createRestaurantRows(meals, context), + )); } } } diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index 9dbfd2773..062fe8a88 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -5,7 +5,9 @@ class RestaurantPageCard extends GenericCard { final String restaurantName; final Widget meals; - RestaurantPageCard(this.restaurantName, this.meals, {super.key}) : super.customStyle(editingMode: false, onDelete: () => null, smallTitle: true); + RestaurantPageCard(this.restaurantName, this.meals, {super.key}) + : super.customStyle( + editingMode: false, onDelete: () => null, smallTitle: true); @override Widget buildCardContent(BuildContext context) { @@ -19,4 +21,4 @@ class RestaurantPageCard extends GenericCard { @override onClick(BuildContext context) {} -} \ No newline at end of file +} diff --git a/uni/lib/view/restaurant/widgets/restaurant_slot.dart b/uni/lib/view/restaurant/widgets/restaurant_slot.dart index 15ecc621e..7f30d6de3 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_slot.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_slot.dart @@ -59,7 +59,8 @@ class RestaurantSlot extends StatelessWidget { child: icon != '' ? SvgPicture.asset( icon, - color: Theme.of(context).primaryColor, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, BlendMode.srcIn), height: 20, ) : null); diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index 89894ba32..8e48c7d7f 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -6,8 +7,8 @@ import 'package:url_launcher/url_launcher.dart'; class ScheduleSlot extends StatelessWidget { final String subject; final String rooms; - final String begin; - final String end; + final DateTime begin; + final DateTime end; final String teacher; final String typeClass; final String? classNumber; @@ -51,14 +52,14 @@ class ScheduleSlot extends StatelessWidget { return Column( key: Key('schedule-slot-time-$begin-$end'), children: [ - createScheduleTime(begin, context), - createScheduleTime(end, context) + createScheduleTime(DateFormat("HH:mm").format(begin), context), + createScheduleTime(DateFormat("HH:mm").format(end), context) ], ); } Widget createScheduleTime(String time, context) => createTextField( - time, Theme.of(context).textTheme.bodyText2, TextAlign.center); + time, Theme.of(context).textTheme.bodyMedium, TextAlign.center); String toUcLink(int occurrId) { const String faculty = 'feup'; //should not be hardcoded @@ -95,13 +96,13 @@ class ScheduleSlot extends StatelessWidget { subject, Theme.of(context) .textTheme - .headline5! + .headlineSmall! .apply(color: Theme.of(context).colorScheme.tertiary), TextAlign.center); final typeClassTextField = createTextField(' ($typeClass)', - Theme.of(context).textTheme.bodyText2, TextAlign.center); + Theme.of(context).textTheme.bodyMedium, TextAlign.center); final roomTextField = createTextField( - rooms, Theme.of(context).textTheme.bodyText2, TextAlign.right); + rooms, Theme.of(context).textTheme.bodyMedium, TextAlign.right); return [ createScheduleSlotTime(context), Expanded( @@ -128,7 +129,7 @@ class ScheduleSlot extends StatelessWidget { Widget createScheduleSlotTeacherClassInfo(context) { return createTextField( classNumber != null ? '$classNumber | $teacher' : teacher, - Theme.of(context).textTheme.bodyText2, + Theme.of(context).textTheme.bodyMedium, TextAlign.center); } diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index e1383ebff..caa578bc4 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -84,17 +84,17 @@ class SplashScreenState extends State { ), child: SizedBox( width: 150.0, - child: SvgPicture.asset( - 'assets/images/logo_dark.svg', - color: Theme.of(context).primaryColor, - ))); + child: SvgPicture.asset('assets/images/logo_dark.svg', + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, BlendMode.srcIn)))); } /// Creates the app main logo Widget createNILogo(BuildContext context) { return SvgPicture.asset( 'assets/images/by_niaefeup.svg', - color: Theme.of(context).primaryColor, + colorFilter: + ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), width: queryData.size.width * 0.45, ); } diff --git a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart index 0b5c3557f..81abb0377 100644 --- a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart +++ b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart @@ -38,7 +38,7 @@ class TermsAndConditionDialog { builder: (BuildContext context) { return AlertDialog( title: Text('Mudança nos Termos e Condições da uni', - style: Theme.of(context).textTheme.headline5), + style: Theme.of(context).textTheme.headlineSmall), content: Column( children: [ Expanded( @@ -91,6 +91,6 @@ class TermsAndConditionDialog { } static TextStyle getTextMethod(BuildContext context) { - return Theme.of(context).textTheme.headline6!; + return Theme.of(context).textTheme.titleLarge!; } } diff --git a/uni/lib/view/theme.dart b/uni/lib/view/theme.dart index f58691c14..8684afef8 100644 --- a/uni/lib/view/theme.dart +++ b/uni/lib/view/theme.dart @@ -5,31 +5,30 @@ const Color lightRed = Color.fromARGB(255, 180, 30, 30); const Color _mildWhite = Color.fromARGB(255, 0xfa, 0xfa, 0xfa); const Color _lightGrey = Color.fromARGB(255, 215, 215, 215); -const Color _grey = Color.fromARGB(255, 0x7f, 0x7f, 0x7f); const Color _strongGrey = Color.fromARGB(255, 90, 90, 90); const Color _mildBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkishBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkBlack = Color.fromARGB(255, 27, 27, 27); const _textTheme = TextTheme( - headline1: TextStyle(fontSize: 40.0, fontWeight: FontWeight.w400), - headline2: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400), - headline3: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400), - headline4: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w300), - headline5: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w400), - headline6: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300), - subtitle1: TextStyle(fontSize: 17.0, fontWeight: FontWeight.w300), - subtitle2: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w300), - bodyText1: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400), - bodyText2: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - caption: TextStyle(fontSize: 13.0, fontWeight: FontWeight.w400), + displayLarge: TextStyle(fontSize: 40.0, fontWeight: FontWeight.w400), + displayMedium: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400), + displaySmall: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400), + headlineMedium: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w300), + headlineSmall: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w400), + titleLarge: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300), + titleMedium: TextStyle(fontSize: 17.0, fontWeight: FontWeight.w300), + titleSmall: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w300), + bodyLarge: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400), + bodyMedium: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), + bodySmall: TextStyle(fontSize: 13.0, fontWeight: FontWeight.w400), ); ThemeData applicationLightTheme = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: darkRed, brightness: Brightness.light, - background: _grey, + background: _mildWhite, primary: darkRed, onPrimary: Colors.white, secondary: darkRed, @@ -39,27 +38,43 @@ ThemeData applicationLightTheme = ThemeData( brightness: Brightness.light, primaryColor: darkRed, textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, + selectionHandleColor: Colors.transparent, ), canvasColor: _mildWhite, - backgroundColor: _mildWhite, scaffoldBackgroundColor: _mildWhite, cardColor: Colors.white, hintColor: _lightGrey, dividerColor: _lightGrey, indicatorColor: darkRed, primaryTextTheme: Typography().black.copyWith( - headline4: const TextStyle(color: _strongGrey), - bodyText1: const TextStyle(color: _strongGrey)), - toggleableActiveColor: darkRed, + headlineMedium: const TextStyle(color: _strongGrey), + bodyLarge: const TextStyle(color: _strongGrey)), iconTheme: const IconThemeData(color: darkRed), - textTheme: _textTheme); + textTheme: _textTheme, + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + ), + trackColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + ), + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + ), + ), + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + ), + )); ThemeData applicationDarkTheme = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: lightRed, brightness: Brightness.dark, - background: _grey, + background: _darkBlack, primary: _lightGrey, onPrimary: _darkishBlack, secondary: _lightGrey, @@ -68,17 +83,30 @@ ThemeData applicationDarkTheme = ThemeData( onTertiary: _darkishBlack), brightness: Brightness.dark, textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, + selectionHandleColor: Colors.transparent, ), primaryColor: _lightGrey, canvasColor: _darkBlack, - backgroundColor: _darkBlack, scaffoldBackgroundColor: _darkBlack, cardColor: _mildBlack, hintColor: _darkishBlack, dividerColor: _strongGrey, indicatorColor: _lightGrey, primaryTextTheme: Typography().white, - toggleableActiveColor: _mildBlack, iconTheme: const IconThemeData(color: _lightGrey), - textTheme: _textTheme.apply(bodyColor: _lightGrey)); + textTheme: _textTheme.apply(bodyColor: _lightGrey), + switchTheme: SwitchThemeData( + trackColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? _lightGrey : null, + ), + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, + ), + ), + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, + ), + )); diff --git a/uni/lib/view/useful_info/widgets/link_button.dart b/uni/lib/view/useful_info/widgets/link_button.dart index 230668485..f333eaa00 100644 --- a/uni/lib/view/useful_info/widgets/link_button.dart +++ b/uni/lib/view/useful_info/widgets/link_button.dart @@ -22,7 +22,7 @@ class LinkButton extends StatelessWidget { child: Text(title, style: Theme.of(context) .textTheme - .headline5! + .headlineSmall! .copyWith(decoration: TextDecoration.underline)), onTap: () => launchUrl(Uri.parse(link)), )) diff --git a/uni/lib/view/useful_info/widgets/text_components.dart b/uni/lib/view/useful_info/widgets/text_components.dart index 9c70eb709..4858559cf 100644 --- a/uni/lib/view/useful_info/widgets/text_components.dart +++ b/uni/lib/view/useful_info/widgets/text_components.dart @@ -9,7 +9,8 @@ Container h1(String text, BuildContext context, {bool initial = false}) { alignment: Alignment.centerLeft, child: Opacity( opacity: 0.8, - child: Text(text, style: Theme.of(context).textTheme.headline5)), + child: + Text(text, style: Theme.of(context).textTheme.headlineSmall)), )); } @@ -18,7 +19,7 @@ Container h2(String text, BuildContext context) { margin: const EdgeInsets.only(top: 13.0, bottom: 0.0, left: 20.0), child: Align( alignment: Alignment.centerLeft, - child: Text(text, style: Theme.of(context).textTheme.subtitle2), + child: Text(text, style: Theme.of(context).textTheme.titleSmall), )); } @@ -34,7 +35,7 @@ Container infoText(String text, BuildContext context, text, style: Theme.of(context) .textTheme - .bodyText1! + .bodyLarge! .apply(color: Theme.of(context).colorScheme.tertiary), ), onTap: () => link != '' ? launchUrl(Uri.parse(link)) : null), diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index fa76beef5..a559742ed 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,18 +20,18 @@ publish_to: 'none' # Remove this line if you wish to 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.5.14+132 +version: 1.5.20+138 environment: sdk: ">=2.17.1 <3.0.0" - flutter: 3.3.2 + flutter: 3.7.2 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -43,12 +43,12 @@ dependencies: encrypt: ^5.0.0-beta.1 path_provider: ^2.0.0 sqflite: ^2.0.3 - path: ^1.8.0 + path: ^1.8.0 cached_network_image: ^3.0.0-nullsafety - flutter_svg: ^1.1.0 + flutter_svg: ^2.0.0+1 synchronized: ^3.0.0 image: ^4.0.13 - connectivity_plus: ^3.0.2 + connectivity_plus: ^3.0.3 logger: ^1.1.0 url_launcher: ^6.0.2 flutter_markdown: ^0.6.0 @@ -61,12 +61,12 @@ dependencies: expansion_tile_card: ^2.0.0 collection: ^1.16.0 timelines: ^0.1.0 - flutter_map: ^2.2.0 + flutter_map: ^3.1.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 latlong2: ^0.8.1 - flutter_map_marker_popup: ^3.2.0 + flutter_map_marker_popup: ^4.0.1 workmanager: ^0.5.1 flutter_local_notifications: ^12.0.4 percent_indicator: ^4.2.2 From 926b78e45dc87382ff6eb09b8aab28a211961c67 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 3 Jul 2023 15:27:33 +0100 Subject: [PATCH 027/100] Fetch classes from all courses --- .../course_units_fetcher/course_units_info_fetcher.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart index d2c5dd147..f2e39788c 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -24,6 +24,8 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { Future> fetchCourseUnitClasses( Session session, int occurrId) async { + List courseUnitClasses = []; + for (String endpoint in getEndpoints(session)) { // Crawl classes from all courses that the course unit is offered in final String courseChoiceUrl = @@ -49,13 +51,13 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { try { final Response response = await NetworkRouter.getWithCookies(url, {}, session); - return parseCourseUnitClasses(response, endpoint); + courseUnitClasses += parseCourseUnitClasses(response, endpoint); } catch (_) { continue; } } } - return []; + return courseUnitClasses; } } From 77eb1235f7e570e6b28b3da8bda58ae9f137b8d1 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Thu, 6 Jul 2023 23:44:04 +0100 Subject: [PATCH 028/100] Remove unused faculties provider --- uni/android/build.gradle | 4 ++-- uni/lib/controller/load_info.dart | 2 -- uni/lib/main.dart | 5 ----- uni/lib/model/providers/exam_provider.dart | 2 +- .../model/providers/state_provider_notifier.dart | 16 ++++++++++++++-- uni/lib/model/providers/state_providers.dart | 6 ------ .../model/providers/user_faculties_provider.dart | 14 -------------- 7 files changed, 17 insertions(+), 32 deletions(-) delete mode 100644 uni/lib/model/providers/user_faculties_provider.dart diff --git a/uni/android/build.gradle b/uni/android/build.gradle index 96de58432..954fa1cd5 100644 --- a/uni/android/build.gradle +++ b/uni/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index 93b7c1a56..32cf8561a 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -106,8 +106,6 @@ void loadLocalUserInfoToState(StateProviders stateProviders, await AppSharedPreferences.getFilteredExams(), Completer()); stateProviders.examProvider .setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); - stateProviders.userFacultiesProvider - .setUserFaculties(await AppSharedPreferences.getUserFaculties()); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '' && diff --git a/uni/lib/main.dart b/uni/lib/main.dart index fa5edf566..9ca04849a 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -21,7 +21,6 @@ import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; -import 'package:uni/model/providers/user_faculties_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/about/about.dart'; import 'package:uni/view/bug_report/bug_report.dart'; @@ -59,7 +58,6 @@ Future main() async { LibraryOccupationProvider(), FacultyLocationsProvider(), LastUserInfoProvider(), - UserFacultiesProvider(), FavoriteCardsProvider(), HomePageEditingModeProvider()); @@ -101,9 +99,6 @@ Future main() async { stateProviders.facultyLocationsProvider), ChangeNotifierProvider( create: (context) => stateProviders.lastUserInfoProvider), - ChangeNotifierProvider( - create: (context) => - stateProviders.userFacultiesProvider), ChangeNotifierProvider( create: (context) => stateProviders.favoriteCardsProvider), diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index 2ba2bc8d3..d9baec552 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -29,7 +29,7 @@ class ExamProvider extends StateProviderNotifier { UnmodifiableMapView get filteredExamsTypes => UnmodifiableMapView(_filteredExamsTypes); - void getUserExams( + Future getUserExams( Completer action, ParserExams parserExams, Tuple2 userPersistentInfo, diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 2e602f197..ba1e500c9 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,9 +1,9 @@ -import 'package:flutter/cupertino.dart'; - +import 'package:flutter/material.dart'; import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { RequestStatus _status = RequestStatus.none; + bool _initialized = false; RequestStatus get status => _status; @@ -11,4 +11,16 @@ abstract class StateProviderNotifier extends ChangeNotifier { _status = status; notifyListeners(); } + + /*void ensureInitialized() { + if (!_initialized) { + _initialized = true; + loadFromStorage(); + loadFromRemote(); + } + } + + void loadFromStorage(); + + void loadFromRemote();*/ } diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index a52545ccd..e6d36398d 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -12,7 +12,6 @@ import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; -import 'package:uni/model/providers/user_faculties_provider.dart'; class StateProviders { final LectureProvider lectureProvider; @@ -25,7 +24,6 @@ class StateProviders { final LibraryOccupationProvider libraryOccupationProvider; final FacultyLocationsProvider facultyLocationsProvider; final LastUserInfoProvider lastUserInfoProvider; - final UserFacultiesProvider userFacultiesProvider; final FavoriteCardsProvider favoriteCardsProvider; final HomePageEditingModeProvider homePageEditingMode; @@ -40,7 +38,6 @@ class StateProviders { this.libraryOccupationProvider, this.facultyLocationsProvider, this.lastUserInfoProvider, - this.userFacultiesProvider, this.favoriteCardsProvider, this.homePageEditingMode); @@ -64,8 +61,6 @@ class StateProviders { Provider.of(context, listen: false); final lastUserInfoProvider = Provider.of(context, listen: false); - final userFacultiesProvider = - Provider.of(context, listen: false); final favoriteCardsProvider = Provider.of(context, listen: false); final homePageEditingMode = @@ -82,7 +77,6 @@ class StateProviders { libraryOccupationProvider, facultyLocationsProvider, lastUserInfoProvider, - userFacultiesProvider, favoriteCardsProvider, homePageEditingMode); } diff --git a/uni/lib/model/providers/user_faculties_provider.dart b/uni/lib/model/providers/user_faculties_provider.dart deleted file mode 100644 index 0d6ba6316..000000000 --- a/uni/lib/model/providers/user_faculties_provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:collection'; - -import 'package:uni/model/providers/state_provider_notifier.dart'; - -class UserFacultiesProvider extends StateProviderNotifier{ - List _faculties = []; - - UnmodifiableListView get faculties => UnmodifiableListView(_faculties); - - setUserFaculties(List faculties){ - _faculties = faculties; - notifyListeners(); - } -} \ No newline at end of file From 3c531b6fd5bcec535de165eceb152ea8b579caf4 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 00:38:34 +0100 Subject: [PATCH 029/100] Implement LazyConsumer --- uni/lib/controller/load_info.dart | 4 +- uni/lib/model/providers/exam_provider.dart | 4 +- .../providers/favorite_cards_provider.dart | 11 ++- .../providers/state_provider_notifier.dart | 19 ++-- .../bus_stop_next_arrivals.dart | 57 +++++------ .../widgets/estimated_arrival_timestamp.dart | 4 +- .../bus_stop_selection.dart | 4 +- uni/lib/view/calendar/calendar.dart | 4 +- .../common_widgets/last_update_timestamp.dart | 4 +- .../request_dependent_widget_builder.dart | 12 +-- uni/lib/view/course_units/course_units.dart | 6 +- uni/lib/view/exams/exams.dart | 32 ++++--- uni/lib/view/home/widgets/bus_stop_card.dart | 12 ++- uni/lib/view/home/widgets/exam_card.dart | 10 +- .../view/home/widgets/restaurant_card.dart | 4 +- uni/lib/view/home/widgets/schedule_card.dart | 42 ++++---- uni/lib/view/lazy_consumer.dart | 22 +++++ uni/lib/view/library/library.dart | 4 +- .../widgets/library_occupation_card.dart | 4 +- uni/lib/view/locations/locations.dart | 6 +- uni/lib/view/login/login.dart | 10 +- uni/lib/view/profile/profile.dart | 39 ++++---- .../profile/widgets/account_info_card.dart | 21 ++-- .../view/profile/widgets/print_info_card.dart | 7 +- .../profile/widgets/profile_overview.dart | 57 ++++++----- .../view/restaurant/restaurant_page_view.dart | 96 +++++++++---------- uni/lib/view/schedule/schedule.dart | 9 +- 27 files changed, 263 insertions(+), 241 deletions(-) create mode 100644 uni/lib/view/lazy_consumer.dart diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index 32cf8561a..fc030c70c 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -99,9 +99,7 @@ void loadLocalUserInfoToState(StateProviders stateProviders, final Tuple2 userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); - Logger().i('Setting up user preferences'); - stateProviders.favoriteCardsProvider - .setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); + //Logger().i('Setting up user preferences'); stateProviders.examProvider.setFilteredExams( await AppSharedPreferences.getFilteredExams(), Completer()); stateProviders.examProvider diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index d9baec552..b0b680acb 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -7,20 +7,18 @@ import 'package:uni/controller/fetchers/exam_fetcher.dart'; import 'package:uni/controller/local_storage/app_exams_database.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/parsers/parser_exams.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/course_unit.dart'; 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 { List _exams = []; List _hiddenExams = []; Map _filteredExamsTypes = {}; - - UnmodifiableListView get exams => UnmodifiableListView(_exams); UnmodifiableListView get hiddenExams => diff --git a/uni/lib/model/providers/favorite_cards_provider.dart b/uni/lib/model/providers/favorite_cards_provider.dart index 99ef9088c..03a326ec4 100644 --- a/uni/lib/model/providers/favorite_cards_provider.dart +++ b/uni/lib/model/providers/favorite_cards_provider.dart @@ -1,10 +1,19 @@ +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/utils/favorite_widget_type.dart'; class FavoriteCardsProvider extends StateProviderNotifier { List _favoriteCards = []; - List get favoriteCards => _favoriteCards.toList(); + List get favoriteCards { + ensureInitialized(); + return _favoriteCards.toList(); + } + + @override + loadFromRemote() async { + setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); + } setFavoriteCards(List favoriteCards) { _favoriteCards = favoriteCards; diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index ba1e500c9..2c6f5a955 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,3 +1,4 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:uni/model/request_status.dart'; @@ -12,15 +13,21 @@ abstract class StateProviderNotifier extends ChangeNotifier { notifyListeners(); } - /*void ensureInitialized() { - if (!_initialized) { - _initialized = true; - loadFromStorage(); + void ensureInitialized() async { + if (_initialized) { + return; + } + + _initialized = true; + loadFromStorage(); + if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { loadFromRemote(); } + + notifyListeners(); } - void loadFromStorage(); + void loadFromStorage() async {} - void loadFromRemote();*/ + void loadFromRemote() async {} } diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 4ff78add8..e99d2d6f0 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/lazy_consumer.dart'; class BusStopNextArrivalsPage extends StatefulWidget { const BusStopNextArrivalsPage({Key? key}) : super(key: key); @@ -21,10 +21,9 @@ class BusStopNextArrivalsPageState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, busProvider, _) => ListView(children: [ - NextArrivals( - busProvider.configuredBusStops, busProvider.status) + NextArrivals(busProvider.configuredBusStops, busProvider.status) ])); } } @@ -34,8 +33,7 @@ class NextArrivals extends StatefulWidget { final Map buses; final RequestStatus busStopStatus; - const NextArrivals(this.buses, this.busStopStatus, - {super.key}); + const NextArrivals(this.buses, this.busStopStatus, {super.key}); @override NextArrivalsState createState() => NextArrivalsState(); @@ -46,33 +44,26 @@ class NextArrivalsState extends State { @override Widget build(BuildContext context) { Widget contentBuilder() { - switch (widget.busStopStatus) { - case RequestStatus.successful: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height, - child: Column(children: requestSuccessful(context))); - case RequestStatus.busy: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height, - child: Column(children: requestBusy(context))); - case RequestStatus.failed: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height, - child: Column(children: requestFailed(context))); - default: - return Container(); + switch (widget.busStopStatus) { + case RequestStatus.successful: + return SizedBox( + height: MediaQuery.of(context).size.height, + child: Column(children: requestSuccessful(context))); + case RequestStatus.busy: + return SizedBox( + height: MediaQuery.of(context).size.height, + child: Column(children: requestBusy(context))); + case RequestStatus.failed: + return SizedBox( + height: MediaQuery.of(context).size.height, + child: Column(children: requestFailed(context))); + default: + return Container(); + } } - } - return DefaultTabController(length: widget.buses.length, child: contentBuilder()); + + return DefaultTabController( + length: widget.buses.length, child: contentBuilder()); } /// Returns a list of widgets for a successfull request diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index c1ec2d134..0fa7c3cdf 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/view/lazy_consumer.dart'; /// Manages the section with the estimated time for the bus arrival class EstimatedArrivalTimeStamp extends StatelessWidget { @@ -13,7 +13,7 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, busProvider, _) => getContent(context, busProvider.timeStamp), ); diff --git a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart index 545fb6e2c..a273c830c 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; @@ -7,6 +6,7 @@ import 'package:uni/view/bus_stop_selection/widgets/bus_stop_search.dart'; import 'package:uni/view/bus_stop_selection/widgets/bus_stop_selection_row.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/lazy_consumer.dart'; class BusStopSelectionPage extends StatefulWidget { const BusStopSelectionPage({super.key}); @@ -36,7 +36,7 @@ class BusStopSelectionPageState @override Widget getBody(BuildContext context) { final width = MediaQuery.of(context).size.width; - return Consumer(builder: (context, busProvider, _) { + return LazyConsumer(builder: (context, busProvider, _) { final List rows = []; busProvider.configuredBusStops.forEach((stopCode, stopData) => rows.add(BusStopSelectionRow(stopCode, stopData))); diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 4942cc005..2af4205c1 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:timelines/timelines.dart'; import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/lazy_consumer.dart'; class CalendarPageView extends StatefulWidget { const CalendarPageView({Key? key}) : super(key: key); @@ -16,7 +16,7 @@ class CalendarPageView extends StatefulWidget { class CalendarPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, calendarProvider, _) => getCalendarPage(context, calendarProvider.calendar), ); diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 4b5138ce2..39f7fec46 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; +import 'package:uni/view/lazy_consumer.dart'; class LastUpdateTimeStamp extends StatefulWidget { const LastUpdateTimeStamp({super.key}); @@ -33,7 +33,7 @@ class _LastUpdateTimeStampState extends State { @override Widget build(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, lastUserInfoProvider, _) => Container( padding: const EdgeInsets.only(top: 8.0, bottom: 10.0), child: _getContent(context, lastUserInfoProvider.lastUpdateTime!)), diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index f5f0b2d1d..a721917c8 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -1,12 +1,11 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; - import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/view/lazy_consumer.dart'; /// Wraps content given its fetch data from the redux store, /// hydrating the component, displaying an empty message, @@ -36,7 +35,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, lastUserInfoProvider, _) { switch (status) { case RequestStatus.successful: @@ -92,9 +91,8 @@ class RequestDependentWidgetBuilder extends StatelessWidget { child: Text('Aconteceu um erro ao carregar os dados', style: Theme.of(context).textTheme.titleMedium))), OutlinedButton( - onPressed: () => - Navigator.pushNamed(context, '/${DrawerItem.navBugReport.title}'), - + onPressed: () => Navigator.pushNamed( + context, '/${DrawerItem.navBugReport.title}'), child: const Text('Reportar erro')) ]); }); diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 19a2b94e4..ba7abcd5e 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -1,14 +1,14 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/course_unit.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/course_units/widgets/course_unit_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; class CourseUnitsPageView extends StatefulWidget { const CourseUnitsPageView({Key? key}) : super(key: key); @@ -28,7 +28,7 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, profileProvider, _) { final List courseUnits = profileProvider.currUcs; List availableYears = []; diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 17f68fa73..2d3420ddc 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/entities/exam.dart'; +import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; -import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/common_widgets/row_container.dart'; -import 'package:uni/view/exams/widgets/exam_row.dart'; import 'package:uni/view/exams/widgets/day_title.dart'; +import 'package:uni/view/exams/widgets/exam_page_title.dart'; +import 'package:uni/view/exams/widgets/exam_row.dart'; +import 'package:uni/view/lazy_consumer.dart'; class ExamsPageView extends StatefulWidget { const ExamsPageView({super.key}); @@ -21,17 +22,17 @@ class ExamsPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, examProvider, _) { - return ListView( - children: [ - Column( - mainAxisSize: MainAxisSize.max, - children: createExamsColumn(context, examProvider.getFilteredExams()), - ) - ], - ); - }); + return LazyConsumer(builder: (context, examProvider, _) { + return ListView( + children: [ + Column( + mainAxisSize: MainAxisSize.max, + children: + createExamsColumn(context, examProvider.getFilteredExams()), + ) + ], + ); + }); } /// Creates a column with all the user's exams. @@ -105,7 +106,8 @@ class ExamsPageViewState extends GeneralPageViewState { } Widget createExamContext(context, Exam exam) { - final isHidden = Provider.of(context).hiddenExams.contains(exam.id); + final isHidden = + Provider.of(context).hiddenExams.contains(exam.id); return Container( key: Key('$exam-exam'), margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index ff567736a..9f778f350 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; +import 'package:uni/view/lazy_consumer.dart'; /// Manages the bus stops card displayed on the user's personal area class BusStopCard extends GenericCard { @@ -24,16 +24,18 @@ class BusStopCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, busProvider, _) { - return getCardContent(context, busProvider.configuredBusStops, busProvider.status); + return getCardContent( + context, busProvider.configuredBusStops, busProvider.status); }, ); } } /// Returns a widget with the bus stop card final content -Widget getCardContent(BuildContext context, Map stopData, busStopStatus) { +Widget getCardContent( + BuildContext context, Map stopData, busStopStatus) { switch (busStopStatus) { case RequestStatus.successful: if (stopData.isNotEmpty) { diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 331a48e26..29e55ff4a 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/common_widgets/row_container.dart'; -import 'package:uni/view/common_widgets/generic_card.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/home/widgets/exam_card_shimmer.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; import 'package:uni/view/exams/widgets/exam_title.dart'; +import 'package:uni/view/home/widgets/exam_card_shimmer.dart'; +import 'package:uni/view/lazy_consumer.dart'; /// Manages the exam card section inside the personal area. class ExamCard extends GenericCard { @@ -32,7 +32,7 @@ class ExamCard extends GenericCard { /// that no exams exist is displayed. @override Widget buildCardContent(BuildContext context) { - return Consumer(builder: (context, examProvider, _) { + return LazyConsumer(builder: (context, examProvider, _) { final filteredExams = examProvider.getFilteredExams(); final hiddenExams = examProvider.hiddenExams; final List exams = filteredExams diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 4eeb035d6..3c8f7f8c8 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/home/widgets/restaurant_row.dart'; +import 'package:uni/view/lazy_consumer.dart'; class RestaurantCard extends GenericCard { RestaurantCard({Key? key}) : super(key: key); @@ -22,7 +22,7 @@ class RestaurantCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, restaurantProvider, _) => RequestDependentWidgetBuilder( context: context, diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index a17fdc2d0..5f6f13ba9 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; -import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; -import 'package:uni/view/schedule/widgets/schedule_slot.dart'; +import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/home/widgets/schedule_card_shimmer.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class ScheduleCard extends GenericCard { ScheduleCard({Key? key}) : super(key: key); @@ -23,20 +23,18 @@ class ScheduleCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( - builder: (context, lectureProvider, _) => RequestDependentWidgetBuilder( - context: context, - status: lectureProvider.status, - contentGenerator: generateSchedule, - content: lectureProvider.lectures, - contentChecker: lectureProvider.lectures.isNotEmpty, - onNullContent: Center( - child: Text('Não existem aulas para apresentar', - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center)), - contentLoadingWidget: const ScheduleCardShimmer().build(context)) - ); - + return LazyConsumer( + builder: (context, lectureProvider, _) => RequestDependentWidgetBuilder( + context: context, + status: lectureProvider.status, + contentGenerator: generateSchedule, + content: lectureProvider.lectures, + contentChecker: lectureProvider.lectures.isNotEmpty, + onNullContent: Center( + child: Text('Não existem aulas para apresentar', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center)), + contentLoadingWidget: const ScheduleCardShimmer().build(context))); } Widget generateSchedule(lectures, BuildContext context) { @@ -58,7 +56,9 @@ class ScheduleCard extends GenericCard { if (now.compareTo(lectures[i].endTime) < 0) { if (lastAddedLectureDate.weekday != lectures[i].startTime.weekday && lastAddedLectureDate.compareTo(lectures[i].startTime) <= 0) { - rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[(lectures[i].startTime.weekday-1) % 7])); + rows.add(DateRectangle( + date: TimeString.getWeekdaysStrings()[ + (lectures[i].startTime.weekday - 1) % 7])); } rows.add(createRowFromLecture(context, lectures[i])); @@ -68,7 +68,9 @@ class ScheduleCard extends GenericCard { } if (rows.isEmpty) { - rows.add(DateRectangle(date: TimeString.getWeekdaysStrings()[lectures[0].startTime.weekday % 7])); + rows.add(DateRectangle( + date: TimeString.getWeekdaysStrings()[ + lectures[0].startTime.weekday % 7])); rows.add(createRowFromLecture(context, lectures[0])); } return rows; diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart new file mode 100644 index 000000000..925368b1d --- /dev/null +++ b/uni/lib/view/lazy_consumer.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; + +class LazyConsumer extends StatelessWidget { + final Widget Function(BuildContext, T, Widget?) builder; + + const LazyConsumer({ + Key? key, + required this.builder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of(context, listen: false).ensureInitialized(); + }); + return Consumer( + builder: builder, + ); + } +} diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 0f4f1d7a1..2756dbc51 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/entities/library_occupation.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; class LibraryPageView extends StatefulWidget { @@ -17,7 +17,7 @@ class LibraryPageView extends StatefulWidget { class LibraryPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, libraryOccupationProvider, _) => LibraryPage(libraryOccupationProvider.occupation)); diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index bcaa96d43..70da1215f 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/percent_indicator.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; +import 'package:uni/view/lazy_consumer.dart'; /// Manages the library card section inside the personal area. class LibraryOccupationCard extends GenericCard { @@ -24,7 +24,7 @@ class LibraryOccupationCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, libraryOccupationProvider, _) => RequestDependentWidgetBuilder( context: context, diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index b9ce8722c..2b2a573e8 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/location_group.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locations/widgets/faculty_maps.dart'; import 'package:uni/view/locations/widgets/map.dart'; import 'package:uni/view/locations/widgets/marker.dart'; @@ -28,7 +28,7 @@ class LocationsPageState extends GeneralPageViewState @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, locationsProvider, _) { return LocationsPageView( locations: locationsProvider.locations, diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 915606c62..ead902fa0 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -4,14 +4,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/login_exceptions.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/providers/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/login/widgets/inputs.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:uni/view/theme.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../lazy_consumer.dart'; class LoginPageView extends StatefulWidget { const LoginPageView({super.key}); @@ -220,7 +222,7 @@ class LoginPageViewState extends State { /// Creates a widget for the user login depending on the status of his login. Widget createStatusWidget(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, sessionProvider, _) { switch (sessionProvider.status) { case RequestStatus.busy: diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index e1165559a..4c884f02a 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/account_info_card.dart'; import 'package:uni/view/profile/widgets/course_info_card.dart'; import 'package:uni/view/profile/widgets/profile_overview.dart'; @@ -18,28 +18,27 @@ class ProfilePageView extends StatefulWidget { class ProfilePageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; - final List courseWidgets = profile.courses.map((e) => [ - CourseInfoCard(course: e), - const Padding(padding: EdgeInsets.all(5.0)) - ]).flattened.toList(); + final List courseWidgets = profile.courses + .map((e) => [ + CourseInfoCard(course: e), + const Padding(padding: EdgeInsets.all(5.0)) + ]) + .flattened + .toList(); - return ListView( - shrinkWrap: false, - children: [ - const Padding(padding: EdgeInsets.all(5.0)), - ProfileOverview( - profile: profile, - getProfileDecorationImage: getProfileDecorationImage - ), - const Padding(padding: EdgeInsets.all(5.0)), - // PrintInfoCard() // TODO: Bring this back when print info is ready again - ...courseWidgets, - AccountInfoCard(), - ] - ); + return ListView(shrinkWrap: false, children: [ + const Padding(padding: EdgeInsets.all(5.0)), + ProfileOverview( + profile: profile, + getProfileDecorationImage: getProfileDecorationImage), + const Padding(padding: EdgeInsets.all(5.0)), + // PrintInfoCard() // TODO: Bring this back when print info is ready again + ...courseWidgets, + AccountInfoCard(), + ]); }, ); } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 47a6363f5..0c3265635 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/tuition_notification_switch.dart'; /// Manages the 'Current account' section inside the user's page (accessible @@ -15,7 +15,7 @@ class AccountInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; return Column(children: [ @@ -49,17 +49,14 @@ class AccountInfoCard extends GenericCard { ]), TableRow(children: [ Container( - margin: - const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), - child: Text("Notificar próxima data limite: ", - style: Theme.of(context).textTheme.titleSmall) - ), + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: Text("Notificar próxima data limite: ", + style: Theme.of(context).textTheme.titleSmall)), Container( - margin: - const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), - child: - const TuitionNotificationSwitch() - ) + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: const TuitionNotificationSwitch()) ]) ]), showLastRefreshedTime(profileStateProvider.feesRefreshTime, context) diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index eb0155295..34bad39f0 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/create_print_mb_dialog.dart'; class PrintInfoCard extends GenericCard { @@ -13,7 +13,7 @@ class PrintInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; return Column( @@ -47,8 +47,7 @@ class PrintInfoCard extends GenericCard { ), onPressed: () => addMoneyDialog(context), child: const Center(child: Icon(Icons.add)), - ) - ), + )), ]) ]), showLastRefreshedTime( diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index 99f142d0b..b1593144b 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -1,52 +1,51 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/view/lazy_consumer.dart'; class ProfileOverview extends StatelessWidget { final Profile profile; final DecorationImage Function(File?) getProfileDecorationImage; - const ProfileOverview({Key? key, required this.profile, - required this.getProfileDecorationImage}) : super(key: key); + const ProfileOverview( + {Key? key, + required this.profile, + required this.getProfileDecorationImage}) + : super(key: key); @override Widget build(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, sessionProvider, _) { return FutureBuilder( future: loadProfilePicture(sessionProvider.session), builder: (BuildContext context, AsyncSnapshot profilePic) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 150.0, - height: 150.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: getProfileDecorationImage(profilePic.data) - ) - ), - const Padding(padding: EdgeInsets.all(8.0)), - Text(profile.name, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.w400) - ), - const Padding(padding: EdgeInsets.all(5.0)), - Text(profile.email, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18.0, fontWeight: FontWeight.w300) - ), - ], - ), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: getProfileDecorationImage(profilePic.data))), + const Padding(padding: EdgeInsets.all(8.0)), + Text(profile.name, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.w400)), + const Padding(padding: EdgeInsets.all(5.0)), + Text(profile.email, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18.0, fontWeight: FontWeight.w300)), + ], + ), ); }, ); } -} \ No newline at end of file +} diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 80704b74e..9b7f02144 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -1,13 +1,12 @@ -import 'package:provider/provider.dart'; -import 'package:uni/model/entities/meal.dart'; import 'package:flutter/material.dart'; +import 'package:uni/model/entities/meal.dart'; +import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; +import 'package:uni/model/utils/day_of_week.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; -import 'package:uni/model/utils/day_of_week.dart'; - -import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/restaurant/widgets/restaurant_page_card.dart'; import 'package:uni/view/restaurant/widgets/restaurant_slot.dart'; @@ -30,60 +29,57 @@ class _RestaurantPageState extends GeneralPageViewState final int weekDay = DateTime.now().weekday; super.initState(); tabController = TabController(vsync: this, length: DayOfWeek.values.length); - tabController.animateTo((tabController.index + (weekDay-1))); + tabController.animateTo((tabController.index + (weekDay - 1))); scrollViewController = ScrollController(); } @override Widget getBody(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, restaurantProvider, _) { - return Column(children: [ - ListView(scrollDirection: Axis.vertical, shrinkWrap: true, children: [ - Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), - alignment: Alignment.center, - child: const PageTitle(name: 'Ementas', center: false, pad: false), - ), - TabBar( - controller: tabController, - isScrollable: true, - tabs: createTabs(context), - ), - ]), - const SizedBox(height: 10), - RequestDependentWidgetBuilder( - context: context, - status: restaurantProvider.status, - contentGenerator: createTabViewBuilder, - content: restaurantProvider.restaurants, - contentChecker: restaurantProvider.restaurants.isNotEmpty, - onNullContent: + return Column(children: [ + ListView(scrollDirection: Axis.vertical, shrinkWrap: true, children: [ + Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), + alignment: Alignment.center, + child: const PageTitle(name: 'Ementas', center: false, pad: false), + ), + TabBar( + controller: tabController, + isScrollable: true, + tabs: createTabs(context), + ), + ]), + const SizedBox(height: 10), + RequestDependentWidgetBuilder( + context: context, + status: restaurantProvider.status, + contentGenerator: createTabViewBuilder, + content: restaurantProvider.restaurants, + contentChecker: restaurantProvider.restaurants.isNotEmpty, + onNullContent: const Center(child: Text('Não há refeições disponíveis.'))) - ]); - } - ); + ]); + }); } Widget createTabViewBuilder(dynamic restaurants, BuildContext context) { - final List dayContents = DayOfWeek.values.map((dayOfWeek) { - List restaurantsWidgets = []; - if (restaurants is List) { - restaurantsWidgets = restaurants - .map((restaurant) => RestaurantPageCard( - restaurant.name, - RestaurantDay(restaurant: restaurant, day: dayOfWeek) - )) - .toList(); - } - return ListView(children: restaurantsWidgets); - }).toList(); + final List dayContents = DayOfWeek.values.map((dayOfWeek) { + List restaurantsWidgets = []; + if (restaurants is List) { + restaurantsWidgets = restaurants + .map((restaurant) => RestaurantPageCard(restaurant.name, + RestaurantDay(restaurant: restaurant, day: dayOfWeek))) + .toList(); + } + return ListView(children: restaurantsWidgets); + }).toList(); - return Expanded( + return Expanded( child: TabBarView( - controller: tabController, - children: dayContents, - )); + controller: tabController, + children: dayContents, + )); } List createTabs(BuildContext context) { @@ -92,7 +88,9 @@ class _RestaurantPageState extends GeneralPageViewState for (var i = 0; i < DayOfWeek.values.length; i++) { tabs.add(Container( color: Theme.of(context).colorScheme.background, - child: Tab(key: Key('cantine-page-tab-$i'), text: toString(DayOfWeek.values[i])), + child: Tab( + key: Key('cantine-page-tab-$i'), + text: toString(DayOfWeek.values[i])), )); } @@ -118,7 +116,7 @@ class RestaurantDay extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: const [ Center( - child: Text("Não há informação disponível sobre refeições")), + child: Text("Não há informação disponível sobre refeições")), ], )); } else { diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 8d9ac565b..971c45eed 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; +import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class SchedulePage extends StatefulWidget { @@ -20,7 +20,7 @@ class SchedulePage extends StatefulWidget { class SchedulePageState extends State { @override Widget build(BuildContext context) { - return Consumer( + return LazyConsumer( builder: (context, lectureProvider, _) { return SchedulePageView( lectures: lectureProvider.lectures, @@ -51,8 +51,7 @@ class SchedulePageView extends StatefulWidget { for (int i = 0; i < daysOfTheWeek.length; i++) { final Set lectures = {}; for (int j = 0; j < schedule.length; j++) { - if (schedule[j].startTime.weekday-1 == i) lectures.add(schedule[j]); - + if (schedule[j].startTime.weekday - 1 == i) lectures.add(schedule[j]); } aggLectures.add(lectures); } From 972a189aff9fb702b70bce270d9b755e96422107 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 01:08:08 +0100 Subject: [PATCH 030/100] Move local data loaders to the providers --- uni/lib/controller/load_info.dart | 33 +--------- .../model/providers/bus_stop_provider.dart | 21 ++++--- .../model/providers/calendar_provider.dart | 5 +- uni/lib/model/providers/exam_provider.dart | 19 +++--- .../providers/faculty_locations_provider.dart | 20 +----- .../providers/favorite_cards_provider.dart | 2 +- .../home_page_editing_mode_provider.dart | 3 + .../providers/last_user_info_provider.dart | 3 +- uni/lib/model/providers/lecture_provider.dart | 17 +++--- .../library_occupation_provider.dart | 17 +++--- .../providers/profile_state_provider.dart | 61 +++++++++---------- .../model/providers/restaurant_provider.dart | 18 +++--- uni/lib/model/providers/session_provider.dart | 13 ++-- .../providers/state_provider_notifier.dart | 15 ++++- 14 files changed, 111 insertions(+), 136 deletions(-) diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index fc030c70c..c50b0a11e 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -72,7 +72,7 @@ Future loadRemoteUserInfoToState(StateProviders stateProviders) async { .getCourseUnitsAndCourseAverages(session, ucs); stateProviders.profileStateProvider .getUserPrintBalance(printBalance, session); - stateProviders.profileStateProvider.getUserFees(fees, session); + stateProviders.profileStateProvider.fetchUserFees(fees, session); }); final allRequests = Future.wait([ @@ -94,37 +94,6 @@ Future loadRemoteUserInfoToState(StateProviders stateProviders) async { return lastUpdate.future; } -void loadLocalUserInfoToState(StateProviders stateProviders, - {skipDatabaseLookup = false}) async { - final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - - //Logger().i('Setting up user preferences'); - stateProviders.examProvider.setFilteredExams( - await AppSharedPreferences.getFilteredExams(), Completer()); - stateProviders.examProvider - .setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); - - if (userPersistentInfo.item1 != '' && - userPersistentInfo.item2 != '' && - !skipDatabaseLookup) { - Logger().i('Fetching local info from database'); - stateProviders.examProvider.updateStateBasedOnLocalUserExams(); - stateProviders.lectureProvider.updateStateBasedOnLocalUserLectures(); - stateProviders.examProvider.updateStateBasedOnLocalUserExams(); - stateProviders.lectureProvider.updateStateBasedOnLocalUserLectures(); - stateProviders.busStopProvider.updateStateBasedOnLocalUserBusStops(); - stateProviders.profileStateProvider.updateStateBasedOnLocalProfile(); - stateProviders.profileStateProvider.updateStateBasedOnLocalRefreshTimes(); - stateProviders.restaurantProvider.updateStateBasedOnLocalRestaurants(); - stateProviders.lastUserInfoProvider.updateStateBasedOnLocalTime(); - stateProviders.calendarProvider.updateStateBasedOnLocalCalendar(); - stateProviders.profileStateProvider.updateStateBasedOnLocalCourseUnits(); - } - - stateProviders.facultyLocationsProvider.getFacultyLocations(Completer()); -} - Future handleRefresh(StateProviders stateProviders) async { await loadRemoteUserInfoToState(stateProviders); } diff --git a/uni/lib/model/providers/bus_stop_provider.dart b/uni/lib/model/providers/bus_stop_provider.dart index 202b6974b..c86d0ea59 100644 --- a/uni/lib/model/providers/bus_stop_provider.dart +++ b/uni/lib/model/providers/bus_stop_provider.dart @@ -4,10 +4,10 @@ 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/request_status.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/entities/trip.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/request_status.dart'; class BusStopProvider extends StateProviderNotifier { Map _configuredBusStops = Map.identity(); @@ -18,6 +18,16 @@ class BusStopProvider extends StateProviderNotifier { DateTime get timeStamp => _timeStamp; + @override + void loadFromStorage() async { + final AppBusStopDatabase busStopsDb = AppBusStopDatabase(); + final Map stops = await busStopsDb.busStops(); + + _configuredBusStops = stops; + notifyListeners(); + getUserBusTrips(Completer()); + } + getUserBusTrips(Completer action) async { updateStatus(RequestStatus.busy); @@ -79,13 +89,4 @@ class BusStopProvider extends StateProviderNotifier { final AppBusStopDatabase db = AppBusStopDatabase(); db.updateFavoriteBusStop(stopCode); } - - updateStateBasedOnLocalUserBusStops() async { - final AppBusStopDatabase busStopsDb = AppBusStopDatabase(); - final Map stops = await busStopsDb.busStops(); - - _configuredBusStops = stops; - notifyListeners(); - getUserBusTrips(Completer()); - } } diff --git a/uni/lib/model/providers/calendar_provider.dart b/uni/lib/model/providers/calendar_provider.dart index 4bcbea2b5..37d5fc7eb 100644 --- a/uni/lib/model/providers/calendar_provider.dart +++ b/uni/lib/model/providers/calendar_provider.dart @@ -4,10 +4,10 @@ import 'dart:collection'; import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/calendar_fetcher_html.dart'; import 'package:uni/controller/local_storage/app_calendar_database.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/calendar_event.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 { List _calendar = []; @@ -32,7 +32,8 @@ class CalendarProvider extends StateProviderNotifier { action.complete(); } - updateStateBasedOnLocalCalendar() async { + @override + void loadFromStorage() async { final CalendarDatabase db = CalendarDatabase(); _calendar = await db.calendar(); notifyListeners(); diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index b0b680acb..4d63f1561 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -27,6 +27,18 @@ class ExamProvider extends StateProviderNotifier { UnmodifiableMapView get filteredExamsTypes => UnmodifiableMapView(_filteredExamsTypes); + @override + void loadFromStorage() async { + setFilteredExams( + await AppSharedPreferences.getFilteredExams(), Completer()); + setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); + + final AppExamsDatabase db = AppExamsDatabase(); + final List exs = await db.exams(); + _exams = exs; + notifyListeners(); + } + Future getUserExams( Completer action, ParserExams parserExams, @@ -61,13 +73,6 @@ class ExamProvider extends StateProviderNotifier { action.complete(); } - updateStateBasedOnLocalUserExams() async { - final AppExamsDatabase db = AppExamsDatabase(); - final List exs = await db.exams(); - _exams = exs; - notifyListeners(); - } - updateFilteredExams() async { final exams = await AppSharedPreferences.getFilteredExams(); _filteredExamsTypes = exams; diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart index 0b3ee6478..981bbc1f3 100644 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ b/uni/lib/model/providers/faculty_locations_provider.dart @@ -1,9 +1,6 @@ -import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/location_fetcher/location_fetcher_asset.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/location_group.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; @@ -13,19 +10,8 @@ class FacultyLocationsProvider extends StateProviderNotifier { UnmodifiableListView get locations => UnmodifiableListView(_locations); - getFacultyLocations(Completer action) async { - try { - updateStatus(RequestStatus.busy); - - _locations = await LocationFetcherAsset().getLocations(); - - notifyListeners(); - updateStatus(RequestStatus.successful); - } catch (e) { - Logger().e('Failed to get locations: ${e.toString()}'); - updateStatus(RequestStatus.failed); - } - - action.complete(); + @override + void loadFromStorage() async { + _locations = await LocationFetcherAsset().getLocations(); } } diff --git a/uni/lib/model/providers/favorite_cards_provider.dart b/uni/lib/model/providers/favorite_cards_provider.dart index 03a326ec4..d4cc583a0 100644 --- a/uni/lib/model/providers/favorite_cards_provider.dart +++ b/uni/lib/model/providers/favorite_cards_provider.dart @@ -11,7 +11,7 @@ class FavoriteCardsProvider extends StateProviderNotifier { } @override - loadFromRemote() async { + loadFromStorage() async { setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); } diff --git a/uni/lib/model/providers/home_page_editing_mode_provider.dart b/uni/lib/model/providers/home_page_editing_mode_provider.dart index e4506f86d..38d135041 100644 --- a/uni/lib/model/providers/home_page_editing_mode_provider.dart +++ b/uni/lib/model/providers/home_page_editing_mode_provider.dart @@ -5,6 +5,9 @@ class HomePageEditingModeProvider extends StateProviderNotifier { bool get isEditing => _isEditing; + @override + void loadFromStorage() {} + setHomePageEditingMode(bool state) { _isEditing = state; notifyListeners(); diff --git a/uni/lib/model/providers/last_user_info_provider.dart b/uni/lib/model/providers/last_user_info_provider.dart index f9774d35c..316429f3f 100644 --- a/uni/lib/model/providers/last_user_info_provider.dart +++ b/uni/lib/model/providers/last_user_info_provider.dart @@ -16,7 +16,8 @@ class LastUserInfoProvider extends StateProviderNotifier { action.complete(); } - updateStateBasedOnLocalTime() async { + @override + void loadFromStorage() async { final AppLastUserInfoUpdateDatabase db = AppLastUserInfoUpdateDatabase(); _lastUpdateTime = await db.getLastUserInfoUpdateTime(); notifyListeners(); diff --git a/uni/lib/model/providers/lecture_provider.dart b/uni/lib/model/providers/lecture_provider.dart index 12c415908..501625a1f 100644 --- a/uni/lib/model/providers/lecture_provider.dart +++ b/uni/lib/model/providers/lecture_provider.dart @@ -7,17 +7,25 @@ import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart'; import 'package:uni/controller/local_storage/app_lectures_database.dart'; -import 'package:uni/model/request_status.dart'; 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 { List _lectures = []; UnmodifiableListView get lectures => UnmodifiableListView(_lectures); + @override + void loadFromStorage() async { + final AppLecturesDatabase db = AppLecturesDatabase(); + final List lecs = await db.lectures(); + _lectures = lecs; + notifyListeners(); + } + void getUserLectures( Completer action, Tuple2 userPersistentInfo, @@ -55,11 +63,4 @@ class LectureProvider extends StateProviderNotifier { .getLectures(session, profile) .catchError((e) => ScheduleFetcherHtml().getLectures(session, profile)); } - - Future updateStateBasedOnLocalUserLectures() async { - final AppLecturesDatabase db = AppLecturesDatabase(); - final List lecs = await db.lectures(); - _lectures = lecs; - notifyListeners(); - } } diff --git a/uni/lib/model/providers/library_occupation_provider.dart b/uni/lib/model/providers/library_occupation_provider.dart index 038cb8ffc..5cd950667 100644 --- a/uni/lib/model/providers/library_occupation_provider.dart +++ b/uni/lib/model/providers/library_occupation_provider.dart @@ -13,6 +13,15 @@ class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupation? get occupation => _occupation; + @override + void loadFromStorage() async { + final LibraryOccupationDatabase db = LibraryOccupationDatabase(); + final LibraryOccupation occupation = await db.occupation(); + + _occupation = occupation; + notifyListeners(); + } + void getLibraryOccupation( Session session, Completer action, @@ -36,12 +45,4 @@ class LibraryOccupationProvider extends StateProviderNotifier { } action.complete(); } - - Future updateStateBasedOnLocalOccupation() async { - final LibraryOccupationDatabase db = LibraryOccupationDatabase(); - final LibraryOccupation occupation = await db.occupation(); - - _occupation = occupation; - notifyListeners(); - } } diff --git a/uni/lib/model/providers/profile_state_provider.dart b/uni/lib/model/providers/profile_state_provider.dart index 619b7a6de..e065dbe0b 100644 --- a/uni/lib/model/providers/profile_state_provider.dart +++ b/uni/lib/model/providers/profile_state_provider.dart @@ -14,12 +14,12 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/app_user_database.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/course_unit.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'; // ignore: always_use_package_imports import '../../controller/fetchers/all_course_units_fetcher.dart'; @@ -39,26 +39,44 @@ class ProfileStateProvider extends StateProviderNotifier { Profile get profile => _profile; - updateStateBasedOnLocalProfile() async { + @override + void loadFromStorage() async { + loadCourses(); + loadBalanceRefreshTimes(); + loadCourseUnits(); + } + + void loadCourses() async { final profileDb = AppUserDataDatabase(); - final Profile profile = await profileDb.getUserData(); + _profile = await profileDb.getUserData(); final AppCoursesDatabase coursesDb = AppCoursesDatabase(); final List courses = await coursesDb.courses(); - profile.courses = courses; + _profile.courses = courses; + } + + void loadBalanceRefreshTimes() async { + final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); + final Map refreshTimes = + await refreshTimesDb.refreshTimes(); - // Build courses states map - final Map coursesStates = {}; - for (Course course in profile.courses) { - coursesStates[course.name!] = course.state!; + final printRefreshTime = refreshTimes['print']; + final feesRefreshTime = refreshTimes['fees']; + if (printRefreshTime != null) { + _printRefreshTime = DateTime.parse(printRefreshTime); } + if (feesRefreshTime != null) { + _feesRefreshTime = DateTime.parse(feesRefreshTime); + } + } - _profile = profile; - notifyListeners(); + void loadCourseUnits() async { + final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); + _currUcs = await db.courseUnits(); } - getUserFees(Completer action, Session session) async { + fetchUserFees(Completer action, Session session) async { try { final response = await FeesFetcher().getUserFeesResponse(session); @@ -134,21 +152,6 @@ class ProfileStateProvider extends StateProviderNotifier { action.complete(); } - updateStateBasedOnLocalRefreshTimes() async { - final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); - final Map refreshTimes = - await refreshTimesDb.refreshTimes(); - - final printRefreshTime = refreshTimes['print']; - final feesRefreshTime = refreshTimes['fees']; - if (printRefreshTime != null) { - _printRefreshTime = DateTime.parse(printRefreshTime); - } - if (feesRefreshTime != null) { - _feesRefreshTime = DateTime.parse(feesRefreshTime); - } - } - getUserInfo(Completer action, Session session) async { try { updateStatus(RequestStatus.busy); @@ -204,10 +207,4 @@ class ProfileStateProvider extends StateProviderNotifier { action.complete(); } - - updateStateBasedOnLocalCourseUnits() async { - final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); - _currUcs = await db.courseUnits(); - notifyListeners(); - } } diff --git a/uni/lib/model/providers/restaurant_provider.dart b/uni/lib/model/providers/restaurant_provider.dart index 54765609d..523e7003c 100644 --- a/uni/lib/model/providers/restaurant_provider.dart +++ b/uni/lib/model/providers/restaurant_provider.dart @@ -2,21 +2,26 @@ import 'dart:async'; import 'dart:collection'; import 'package:logger/logger.dart'; +import 'package:uni/controller/fetchers/restaurant_fetcher.dart'; import 'package:uni/controller/local_storage/app_restaurant_database.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'; -import 'package:uni/controller/fetchers/restaurant_fetcher.dart'; - class RestaurantProvider extends StateProviderNotifier { List _restaurants = []; UnmodifiableListView get restaurants => UnmodifiableListView(_restaurants); + @override + Future loadFromStorage() async { + final RestaurantDatabase restaurantDb = RestaurantDatabase(); + final List restaurants = await restaurantDb.getRestaurants(); + _restaurants = restaurants; + } + void getRestaurantsFromFetcher( Completer action, Session session) async { try { @@ -36,11 +41,4 @@ class RestaurantProvider extends StateProviderNotifier { } action.complete(); } - - void updateStateBasedOnLocalRestaurants() async { - final RestaurantDatabase restaurantDb = RestaurantDatabase(); - final List restaurants = await restaurantDb.getRestaurants(); - _restaurants = restaurants; - notifyListeners(); - } } diff --git a/uni/lib/model/providers/session_provider.dart b/uni/lib/model/providers/session_provider.dart index 44cf54aa1..82ac9fb8f 100644 --- a/uni/lib/model/providers/session_provider.dart +++ b/uni/lib/model/providers/session_provider.dart @@ -8,10 +8,10 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_session.dart'; import 'package:uni/model/entities/login_exceptions.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/providers/state_providers.dart'; +import 'package:uni/model/request_status.dart'; class SessionProvider extends StateProviderNotifier { Session _session = Session(); @@ -22,6 +22,9 @@ class SessionProvider extends StateProviderNotifier { UnmodifiableListView get faculties => UnmodifiableListView(_faculties); + @override + void loadFromStorage() {} + login( Completer action, String username, @@ -46,7 +49,7 @@ class SessionProvider extends StateProviderNotifier { Future.delayed(const Duration(seconds: 20), () => {NotificationManager().initializeNotifications()}); - loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); + //loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); await loadRemoteUserInfoToState(stateProviders); usernameController.clear(); @@ -59,7 +62,7 @@ class SessionProvider extends StateProviderNotifier { await NetworkRouter.loginInSigarra(username, password, faculties); if (isPasswordExpired(responseHtml)) { action.completeError(ExpiredCredentialsException()); - }else{ + } else { action.completeError(WrongCredentialsException()); } updateStatus(RequestStatus.failed); @@ -78,10 +81,10 @@ class SessionProvider extends StateProviderNotifier { StateProviders stateProviders, {Completer? action}) async { try { - loadLocalUserInfoToState(stateProviders); + //loadLocalUserInfoToState(stateProviders); updateStatus(RequestStatus.busy); _session = await NetworkRouter.login(username, password, faculties, true); - notifyListeners(); + //notifyListeners(); if (session.authenticated) { await loadRemoteUserInfoToState(stateProviders); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 2c6f5a955..ea623961b 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,5 +1,6 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { @@ -19,7 +20,15 @@ abstract class StateProviderNotifier extends ChangeNotifier { } _initialized = true; - loadFromStorage(); + + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + final sessionIsPersistent = + userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; + if (sessionIsPersistent) { + loadFromStorage(); + } + if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { loadFromRemote(); } @@ -27,7 +36,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { notifyListeners(); } - void loadFromStorage() async {} + void loadFromStorage(); - void loadFromRemote() async {} + void loadFromRemote() {} } From b1e10700f296ec34cb007d56a19cf004795d7e5d Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 01:58:14 +0100 Subject: [PATCH 031/100] Move remote fetching logic to providers --- uni/lib/controller/load_info.dart | 74 ++------ uni/lib/main.dart | 14 +- uni/lib/model/entities/profile.dart | 7 +- .../model/providers/bus_stop_provider.dart | 7 + .../model/providers/calendar_provider.dart | 8 + uni/lib/model/providers/exam_provider.dart | 14 +- .../providers/faculty_locations_provider.dart | 6 + .../providers/favorite_cards_provider.dart | 10 +- .../home_page_editing_mode_provider.dart | 5 + .../providers/last_user_info_provider.dart | 5 + uni/lib/model/providers/lecture_provider.dart | 16 +- .../library_occupation_provider.dart | 8 + ...te_provider.dart => profile_provider.dart} | 37 ++-- .../model/providers/restaurant_provider.dart | 8 + uni/lib/model/providers/session_provider.dart | 8 +- .../providers/state_provider_notifier.dart | 8 +- uni/lib/model/providers/state_providers.dart | 6 +- uni/lib/view/course_units/course_units.dart | 7 +- uni/lib/view/lazy_consumer.dart | 10 +- uni/lib/view/profile/profile.dart | 4 +- .../profile/widgets/account_info_card.dart | 4 +- .../view/profile/widgets/print_info_card.dart | 4 +- uni/test/integration/src/exams_page_test.dart | 33 ++-- .../integration/src/schedule_page_test.dart | 159 +++++++++--------- .../unit/providers/exams_provider_test.dart | 60 ++++--- .../unit/providers/lecture_provider_test.dart | 6 +- 26 files changed, 298 insertions(+), 230 deletions(-) rename uni/lib/model/providers/{profile_state_provider.dart => profile_provider.dart} (86%) diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index c50b0a11e..746af7fd6 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -1,16 +1,12 @@ import 'dart:async'; import 'dart:io'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:logger/logger.dart'; -import 'package:tuple/tuple.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; -import 'package:uni/controller/parsers/parser_exams.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_providers.dart'; -Future loadReloginInfo(StateProviders stateProviders) async { +/*Future loadReloginInfo(StateProviders stateProviders) async { final Tuple2 userPersistentCredentials = await AppSharedPreferences.getPersistentUserInfo(); final String userName = userPersistentCredentials.item1; @@ -24,10 +20,10 @@ Future loadReloginInfo(StateProviders stateProviders) async { return action.future; } return Future.error('No credentials stored'); -} +}*/ -Future loadRemoteUserInfoToState(StateProviders stateProviders) async { - if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { +Future loadUserProfileInfoFromRemote(StateProviders stateProviders) async { + /*if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { return; } @@ -36,66 +32,18 @@ Future loadRemoteUserInfoToState(StateProviders stateProviders) async { final session = stateProviders.sessionProvider.session; if (!session.authenticated && session.persistentSession) { await loadReloginInfo(stateProviders); - } - - final Completer userInfo = Completer(), - ucs = Completer(), - exams = Completer(), - schedule = Completer(), - printBalance = Completer(), - fees = Completer(), - trips = Completer(), - lastUpdate = Completer(), - restaurants = Completer(), - libraryOccupation = Completer(), - calendar = Completer(); - - stateProviders.profileStateProvider.getUserInfo(userInfo, session); - stateProviders.busStopProvider.getUserBusTrips(trips); - stateProviders.restaurantProvider - .getRestaurantsFromFetcher(restaurants, session); - stateProviders.calendarProvider.getCalendarFromFetcher(session, calendar); - stateProviders.libraryOccupationProvider - .getLibraryOccupation(session, libraryOccupation); - - final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + }*/ - userInfo.future.then((value) { - final profile = stateProviders.profileStateProvider.profile; - final currUcs = stateProviders.profileStateProvider.currUcs; - stateProviders.examProvider.getUserExams( - exams, ParserExams(), userPersistentInfo, profile, session, currUcs); - stateProviders.lectureProvider - .getUserLectures(schedule, userPersistentInfo, session, profile); - stateProviders.profileStateProvider - .getCourseUnitsAndCourseAverages(session, ucs); - stateProviders.profileStateProvider - .getUserPrintBalance(printBalance, session); - stateProviders.profileStateProvider.fetchUserFees(fees, session); - }); + stateProviders.profileStateProvider + .fetchUserInfo(Completer(), stateProviders.sessionProvider.session); - final allRequests = Future.wait([ - ucs.future, - exams.future, - schedule.future, - printBalance.future, - fees.future, - userInfo.future, - trips.future, - restaurants.future, - libraryOccupation.future, - calendar.future - ]); - allRequests.then((futures) { - stateProviders.lastUserInfoProvider - .setLastUserInfoUpdateTimestamp(lastUpdate); - }); - return lastUpdate.future; + stateProviders.lastUserInfoProvider + .setLastUserInfoUpdateTimestamp(Completer()); } Future handleRefresh(StateProviders stateProviders) async { - await loadRemoteUserInfoToState(stateProviders); + Logger().e('TODO: handleRefresh'); + // await loadRemoteUserInfoToState(stateProviders); } Future loadProfilePicture(Session session, {forceRetrieval = false}) { diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 9ca04849a..aea2edfae 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -17,7 +17,7 @@ import 'package:uni/model/providers/home_page_editing_mode_provider.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; @@ -30,10 +30,10 @@ import 'package:uni/view/common_widgets/page_transition.dart'; import 'package:uni/view/course_units/course_units.dart'; import 'package:uni/view/exams/exams.dart'; import 'package:uni/view/home/home.dart'; +import 'package:uni/view/library/library.dart'; import 'package:uni/view/locations/locations.dart'; import 'package:uni/view/logout_route.dart'; import 'package:uni/view/navigation_service.dart'; -import 'package:uni/view/library/library.dart'; import 'package:uni/view/restaurant/restaurant_page_view.dart'; import 'package:uni/view/schedule/schedule.dart'; import 'package:uni/view/splash/splash.dart'; @@ -52,7 +52,7 @@ Future main() async { ExamProvider(), BusStopProvider(), RestaurantProvider(), - ProfileStateProvider(), + ProfileProvider(), SessionProvider(), CalendarProvider(), LibraryOccupationProvider(), @@ -64,10 +64,10 @@ Future main() async { OnStartUp.onStart(stateProviders.sessionProvider); WidgetsFlutterBinding.ensureInitialized(); - await Workmanager().initialize(workerStartCallback, - isInDebugMode: !kReleaseMode // run workmanager in debug mode when app is in debug mode - ); - + await Workmanager().initialize(workerStartCallback, + isInDebugMode: + !kReleaseMode // run workmanager in debug mode when app is in debug mode + ); final savedTheme = await AppSharedPreferences.getThemeMode(); await SentryFlutter.init((options) { diff --git a/uni/lib/model/entities/profile.dart b/uni/lib/model/entities/profile.dart index 6c222c657..b5fac913d 100644 --- a/uni/lib/model/entities/profile.dart +++ b/uni/lib/model/entities/profile.dart @@ -2,15 +2,17 @@ import 'dart:convert'; import 'package:tuple/tuple.dart'; import 'package:uni/model/entities/course.dart'; +import 'package:uni/model/entities/course_unit.dart'; /// Stores information about the user's profile. class Profile { final String name; final String email; - late List courses; final String printBalance; final String feesBalance; final String feesLimit; + late List courses; + late List currentCourseUnits; Profile( {this.name = '', @@ -19,7 +21,8 @@ class Profile { this.printBalance = '', this.feesBalance = '', this.feesLimit = ''}) - : courses = courses ?? []; + : courses = courses ?? [], + currentCourseUnits = []; /// Creates a new instance from a JSON object. static Profile fromResponse(dynamic response) { diff --git a/uni/lib/model/providers/bus_stop_provider.dart b/uni/lib/model/providers/bus_stop_provider.dart index c86d0ea59..302347b64 100644 --- a/uni/lib/model/providers/bus_stop_provider.dart +++ b/uni/lib/model/providers/bus_stop_provider.dart @@ -5,6 +5,8 @@ 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/entities/trip.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; @@ -28,6 +30,11 @@ class BusStopProvider extends StateProviderNotifier { getUserBusTrips(Completer()); } + @override + void loadFromRemote(Session session, Profile profile) { + getUserBusTrips(Completer()); + } + getUserBusTrips(Completer action) async { updateStatus(RequestStatus.busy); diff --git a/uni/lib/model/providers/calendar_provider.dart b/uni/lib/model/providers/calendar_provider.dart index 37d5fc7eb..281e1b743 100644 --- a/uni/lib/model/providers/calendar_provider.dart +++ b/uni/lib/model/providers/calendar_provider.dart @@ -5,6 +5,7 @@ import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/calendar_fetcher_html.dart'; import 'package:uni/controller/local_storage/app_calendar_database.dart'; 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'; @@ -15,6 +16,13 @@ class CalendarProvider extends StateProviderNotifier { UnmodifiableListView get calendar => UnmodifiableListView(_calendar); + @override + Future loadFromRemote(Session session, Profile profile) async { + final Completer action = Completer(); + getCalendarFromFetcher(session, action); + await action.future; + } + getCalendarFromFetcher(Session session, Completer action) async { try { updateStatus(RequestStatus.busy); diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index 4d63f1561..1192abfdb 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -39,7 +39,19 @@ class ExamProvider extends StateProviderNotifier { notifyListeners(); } - Future getUserExams( + @override + void loadFromRemote(Session session, Profile profile) async { + final Completer action = Completer(); + final ParserExams parserExams = ParserExams(); + final Tuple2 userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + + fetchUserExams(action, parserExams, userPersistentInfo, profile, session, + profile.currentCourseUnits); + await action.future; + } + + Future fetchUserExams( Completer action, ParserExams parserExams, Tuple2 userPersistentInfo, diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart index 981bbc1f3..0e1af6403 100644 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ b/uni/lib/model/providers/faculty_locations_provider.dart @@ -4,6 +4,9 @@ import 'package:uni/controller/fetchers/location_fetcher/location_fetcher_asset. import 'package:uni/model/entities/location_group.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import '../entities/profile.dart'; +import '../entities/session.dart'; + class FacultyLocationsProvider extends StateProviderNotifier { List _locations = []; @@ -14,4 +17,7 @@ class FacultyLocationsProvider extends StateProviderNotifier { void loadFromStorage() async { _locations = await LocationFetcherAsset().getLocations(); } + + @override + Future loadFromRemote(Session session, Profile profile) async {} } diff --git a/uni/lib/model/providers/favorite_cards_provider.dart b/uni/lib/model/providers/favorite_cards_provider.dart index d4cc583a0..79be2a624 100644 --- a/uni/lib/model/providers/favorite_cards_provider.dart +++ b/uni/lib/model/providers/favorite_cards_provider.dart @@ -1,20 +1,22 @@ 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/utils/favorite_widget_type.dart'; class FavoriteCardsProvider extends StateProviderNotifier { List _favoriteCards = []; - List get favoriteCards { - ensureInitialized(); - return _favoriteCards.toList(); - } + List get favoriteCards => _favoriteCards.toList(); @override loadFromStorage() async { setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); } + @override + Future loadFromRemote(Session session, Profile profile) async {} + setFavoriteCards(List favoriteCards) { _favoriteCards = favoriteCards; notifyListeners(); diff --git a/uni/lib/model/providers/home_page_editing_mode_provider.dart b/uni/lib/model/providers/home_page_editing_mode_provider.dart index 38d135041..e45457874 100644 --- a/uni/lib/model/providers/home_page_editing_mode_provider.dart +++ b/uni/lib/model/providers/home_page_editing_mode_provider.dart @@ -1,3 +1,5 @@ +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; class HomePageEditingModeProvider extends StateProviderNotifier { @@ -8,6 +10,9 @@ class HomePageEditingModeProvider extends StateProviderNotifier { @override void loadFromStorage() {} + @override + Future loadFromRemote(Session session, Profile profile) async {} + setHomePageEditingMode(bool state) { _isEditing = state; notifyListeners(); diff --git a/uni/lib/model/providers/last_user_info_provider.dart b/uni/lib/model/providers/last_user_info_provider.dart index 316429f3f..724091265 100644 --- a/uni/lib/model/providers/last_user_info_provider.dart +++ b/uni/lib/model/providers/last_user_info_provider.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; class LastUserInfoProvider extends StateProviderNotifier { @@ -22,4 +24,7 @@ class LastUserInfoProvider extends StateProviderNotifier { _lastUpdateTime = await db.getLastUserInfoUpdateTime(); notifyListeners(); } + + @override + Future loadFromRemote(Session session, Profile profile) async {} } diff --git a/uni/lib/model/providers/lecture_provider.dart b/uni/lib/model/providers/lecture_provider.dart index 501625a1f..d1e26fa96 100644 --- a/uni/lib/model/providers/lecture_provider.dart +++ b/uni/lib/model/providers/lecture_provider.dart @@ -7,6 +7,7 @@ import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart'; import 'package:uni/controller/local_storage/app_lectures_database.dart'; +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; @@ -21,12 +22,21 @@ class LectureProvider extends StateProviderNotifier { @override void loadFromStorage() async { final AppLecturesDatabase db = AppLecturesDatabase(); - final List lecs = await db.lectures(); - _lectures = lecs; + final List lectures = await db.lectures(); + _lectures = lectures; notifyListeners(); } - void getUserLectures( + @override + Future loadFromRemote(Session session, Profile profile) async { + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + final Completer action = Completer(); + fetchUserLectures(action, userPersistentInfo, session, profile); + await action.future; + } + + void fetchUserLectures( Completer action, Tuple2 userPersistentInfo, Session session, diff --git a/uni/lib/model/providers/library_occupation_provider.dart b/uni/lib/model/providers/library_occupation_provider.dart index 5cd950667..31b8092a1 100644 --- a/uni/lib/model/providers/library_occupation_provider.dart +++ b/uni/lib/model/providers/library_occupation_provider.dart @@ -4,6 +4,7 @@ import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/library_occupation_fetcher.dart'; import 'package:uni/controller/local_storage/app_library_occupation_database.dart'; 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'; @@ -22,6 +23,13 @@ class LibraryOccupationProvider extends StateProviderNotifier { notifyListeners(); } + @override + Future loadFromRemote(Session session, Profile profile) async { + final Completer action = Completer(); + getLibraryOccupation(session, action); + await action.future; + } + void getLibraryOccupation( Session session, Completer action, diff --git a/uni/lib/model/providers/profile_state_provider.dart b/uni/lib/model/providers/profile_provider.dart similarity index 86% rename from uni/lib/model/providers/profile_state_provider.dart rename to uni/lib/model/providers/profile_provider.dart index e065dbe0b..35bb27a9f 100644 --- a/uni/lib/model/providers/profile_state_provider.dart +++ b/uni/lib/model/providers/profile_provider.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:collection'; import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; @@ -15,7 +14,6 @@ import 'package:uni/controller/local_storage/app_user_database.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_unit.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; @@ -24,15 +22,11 @@ import 'package:uni/model/request_status.dart'; // ignore: always_use_package_imports import '../../controller/fetchers/all_course_units_fetcher.dart'; -class ProfileStateProvider extends StateProviderNotifier { - List _currUcs = []; +class ProfileProvider extends StateProviderNotifier { Profile _profile = Profile(); DateTime? _feesRefreshTime; DateTime? _printRefreshTime; - UnmodifiableListView get currUcs => - UnmodifiableListView(_currUcs); - String get feesRefreshTime => _feesRefreshTime.toString(); String get printRefreshTime => _printRefreshTime.toString(); @@ -46,6 +40,20 @@ class ProfileStateProvider extends StateProviderNotifier { loadCourseUnits(); } + @override + Future loadFromRemote(Session session, Profile profile) async { + final Completer userFeesAction = Completer(); + fetchUserFees(userFeesAction, session); + + final Completer printBalanceAction = Completer(); + fetchUserPrintBalance(printBalanceAction, session); + + final Completer courseUnitsAction = Completer(); + fetchCourseUnitsAndCourseAverages(session, courseUnitsAction); + + await Future.wait([userFeesAction.future, printBalanceAction.future]); + } + void loadCourses() async { final profileDb = AppUserDataDatabase(); _profile = await profileDb.getUserData(); @@ -73,7 +81,7 @@ class ProfileStateProvider extends StateProviderNotifier { void loadCourseUnits() async { final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); - _currUcs = await db.courseUnits(); + profile.currentCourseUnits = await db.courseUnits(); } fetchUserFees(Completer action, Session session) async { @@ -118,7 +126,7 @@ class ProfileStateProvider extends StateProviderNotifier { refreshTimesDatabase.saveRefreshTime(db, currentTime); } - getUserPrintBalance(Completer action, Session session) async { + fetchUserPrintBalance(Completer action, Session session) async { try { final response = await PrintFetcher().getUserPrintsResponse(session); final String printBalance = await getPrintsBalance(response); @@ -152,7 +160,7 @@ class ProfileStateProvider extends StateProviderNotifier { action.complete(); } - getUserInfo(Completer action, Session session) async { + fetchUserInfo(Completer action, Session session) async { try { updateStatus(RequestStatus.busy); @@ -162,7 +170,7 @@ class ProfileStateProvider extends StateProviderNotifier { final ucs = CurrentCourseUnitsFetcher() .getCurrentCourseUnits(session) - .then((res) => _currUcs = res); + .then((res) => _profile.currentCourseUnits = res); await Future.wait([profile, ucs]); notifyListeners(); updateStatus(RequestStatus.successful); @@ -181,12 +189,12 @@ class ProfileStateProvider extends StateProviderNotifier { action.complete(); } - getCourseUnitsAndCourseAverages( + fetchCourseUnitsAndCourseAverages( Session session, Completer action) async { updateStatus(RequestStatus.busy); try { final List courses = profile.courses; - _currUcs = await AllCourseUnitsFetcher() + _profile.currentCourseUnits = await AllCourseUnitsFetcher() .getAllCourseUnitsAndCourseAverages(courses, session); updateStatus(RequestStatus.successful); notifyListeners(); @@ -198,7 +206,8 @@ class ProfileStateProvider extends StateProviderNotifier { await coursesDb.saveNewCourses(courses); final courseUnitsDatabase = AppCourseUnitsDatabase(); - await courseUnitsDatabase.saveNewCourseUnits(currUcs); + await courseUnitsDatabase + .saveNewCourseUnits(_profile.currentCourseUnits); } } catch (e) { Logger().e('Failed to get all user ucs: $e'); diff --git a/uni/lib/model/providers/restaurant_provider.dart b/uni/lib/model/providers/restaurant_provider.dart index 523e7003c..d6a7a7556 100644 --- a/uni/lib/model/providers/restaurant_provider.dart +++ b/uni/lib/model/providers/restaurant_provider.dart @@ -4,6 +4,7 @@ import 'dart:collection'; import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/restaurant_fetcher.dart'; import 'package:uni/controller/local_storage/app_restaurant_database.dart'; +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'; @@ -22,6 +23,13 @@ class RestaurantProvider extends StateProviderNotifier { _restaurants = restaurants; } + @override + Future loadFromRemote(Session session, Profile profile) async { + final Completer action = Completer(); + getRestaurantsFromFetcher(action, session); + await action.future; + } + void getRestaurantsFromFetcher( Completer action, Session session) async { try { diff --git a/uni/lib/model/providers/session_provider.dart b/uni/lib/model/providers/session_provider.dart index 82ac9fb8f..5ef686b5d 100644 --- a/uni/lib/model/providers/session_provider.dart +++ b/uni/lib/model/providers/session_provider.dart @@ -8,6 +8,7 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_session.dart'; import 'package:uni/model/entities/login_exceptions.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/providers/state_providers.dart'; @@ -25,6 +26,9 @@ class SessionProvider extends StateProviderNotifier { @override void loadFromStorage() {} + @override + Future loadFromRemote(Session session, Profile profile) async {} + login( Completer action, String username, @@ -50,7 +54,7 @@ class SessionProvider extends StateProviderNotifier { () => {NotificationManager().initializeNotifications()}); //loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); - await loadRemoteUserInfoToState(stateProviders); + await loadUserProfileInfoFromRemote(stateProviders); usernameController.clear(); passwordController.clear(); @@ -87,7 +91,7 @@ class SessionProvider extends StateProviderNotifier { //notifyListeners(); if (session.authenticated) { - await loadRemoteUserInfoToState(stateProviders); + await loadUserProfileInfoFromRemote(stateProviders); Future.delayed(const Duration(seconds: 20), () => {NotificationManager().initializeNotifications()}); updateStatus(RequestStatus.successful); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index ea623961b..b818caddc 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,6 +1,8 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; 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/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { @@ -14,7 +16,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { notifyListeners(); } - void ensureInitialized() async { + void ensureInitialized(Session session, Profile profile) async { if (_initialized) { return; } @@ -30,7 +32,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { } if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - loadFromRemote(); + loadFromRemote(session, profile); } notifyListeners(); @@ -38,5 +40,5 @@ abstract class StateProviderNotifier extends ChangeNotifier { void loadFromStorage(); - void loadFromRemote() {} + void loadFromRemote(Session session, Profile profile); } diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index e6d36398d..f8a83dcbc 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -9,7 +9,7 @@ import 'package:uni/model/providers/home_page_editing_mode_provider.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; @@ -18,7 +18,7 @@ class StateProviders { final ExamProvider examProvider; final BusStopProvider busStopProvider; final RestaurantProvider restaurantProvider; - final ProfileStateProvider profileStateProvider; + final ProfileProvider profileStateProvider; final SessionProvider sessionProvider; final CalendarProvider calendarProvider; final LibraryOccupationProvider libraryOccupationProvider; @@ -50,7 +50,7 @@ class StateProviders { final restaurantProvider = Provider.of(context, listen: false); final profileStateProvider = - Provider.of(context, listen: false); + Provider.of(context, listen: false); final sessionProvider = Provider.of(context, listen: false); final calendarProvider = diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index ba7abcd5e..678e79ea5 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:uni/model/entities/course_unit.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; @@ -28,9 +28,10 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { - return LazyConsumer( + return LazyConsumer( builder: (context, profileProvider, _) { - final List courseUnits = profileProvider.currUcs; + final List courseUnits = + profileProvider.profile.currentCourseUnits; List availableYears = []; List availableSemesters = []; if (courseUnits.isNotEmpty) { diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 925368b1d..0027c7522 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -1,5 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; +import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; class LazyConsumer extends StatelessWidget { @@ -12,9 +14,15 @@ class LazyConsumer extends StatelessWidget { @override Widget build(BuildContext context) { + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; WidgetsBinding.instance.addPostFrameCallback((_) { - Provider.of(context, listen: false).ensureInitialized(); + Provider.of(context, listen: false) + .ensureInitialized(session, profile); }); + return Consumer( builder: builder, ); diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 4c884f02a..1c988249e 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/account_info_card.dart'; @@ -18,7 +18,7 @@ class ProfilePageView extends StatefulWidget { class ProfilePageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; final List courseWidgets = profile.courses diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 0c3265635..4ffbcbbee 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/tuition_notification_switch.dart'; @@ -15,7 +15,7 @@ class AccountInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; return Column(children: [ diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index 34bad39f0..264a85689 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/profile_state_provider.dart'; +import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/create_print_mb_dialog.dart'; @@ -13,7 +13,7 @@ class PrintInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( + return LazyConsumer( builder: (context, profileStateProvider, _) { final profile = profileStateProvider.profile; return Column( diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index f8adeed87..f3d83b34d 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; @@ -29,25 +30,33 @@ void main() { final mockClient = MockClient(); final mockResponse = MockResponse(); final sopeCourseUnit = CourseUnit( - abbreviation: 'SOPE', occurrId: 0, name: 'Sistemas Operativos', status: 'V'); + abbreviation: 'SOPE', + occurrId: 0, + name: 'Sistemas Operativos', + status: 'V'); final sdisCourseUnit = CourseUnit( - abbreviation: 'SDIS', name: 'Sistemas Distribuídos', occurrId: 0, status: 'V'); + abbreviation: 'SDIS', + name: 'Sistemas Distribuídos', + occurrId: 0, + status: 'V'); final DateTime beginSopeExam = DateTime.parse('2099-11-18 17:00'); final DateTime endSopeExam = DateTime.parse('2099-11-18 19:00'); - final sopeExam = Exam('44426', beginSopeExam, endSopeExam, 'SOPE', [], 'MT', 'feup'); + final sopeExam = + Exam('44426', beginSopeExam, endSopeExam, 'SOPE', [], 'MT', 'feup'); final DateTime beginSdisExam = DateTime.parse('2099-10-21 17:00'); final DateTime endSdisExam = DateTime.parse('2099-10-21 19:00'); - final sdisExam = Exam('44425', beginSdisExam, endSdisExam, 'SDIS',[], 'MT', 'feup'); + final sdisExam = + Exam('44425', beginSdisExam, endSdisExam, 'SDIS', [], 'MT', 'feup'); final DateTime beginMdisExam = DateTime.parse('2099-10-22 17:00'); final DateTime endMdisExam = DateTime.parse('2099-10-22 19:00'); - final mdisExam = Exam('44429', beginMdisExam, endMdisExam, 'MDIS',[], 'MT', 'feup'); - + final mdisExam = + Exam('44429', beginMdisExam, endMdisExam, 'MDIS', [], 'MT', 'feup'); + final Map filteredExams = {}; - for(String type in Exam.displayedTypes) { + for (String type in Exam.displayedTypes) { filteredExams[type] = true; } - final profile = Profile(); profile.courses = [Course(id: 7474, faculty: 'feup')]; @@ -75,7 +84,7 @@ void main() { expect(find.byKey(Key('$mdisExam-exam')), findsNothing); final Completer completer = Completer(); - examProvider.getUserExams( + examProvider.fetchUserExams( completer, ParserExams(), const Tuple2('', ''), @@ -114,7 +123,7 @@ void main() { expect(find.byKey(Key('$sopeExam-exam')), findsNothing); final Completer completer = Completer(); - examProvider.getUserExams( + examProvider.fetchUserExams( completer, ParserExams(), const Tuple2('', ''), @@ -128,7 +137,7 @@ void main() { expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); expect(find.byIcon(Icons.filter_alt), findsOneWidget); - + final Completer settingFilteredExams = Completer(); filteredExams['ExamDoesNotExist'] = true; examProvider.setFilteredExams(filteredExams, settingFilteredExams); @@ -160,7 +169,7 @@ void main() { expect(okButton, findsOneWidget); await tester.tap(okButton); - + await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsNothing); diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index 495f14fe0..ba74d834a 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; @@ -32,91 +33,91 @@ class UriMatcher extends CustomMatcher { } void main() { - group('SchedulePage Integration Tests', () { - final mockClient = MockClient(); - final mockResponse = MockResponse(); - final badMockResponse = MockResponse(); + group('SchedulePage Integration Tests', () { + final mockClient = MockClient(); + final mockResponse = MockResponse(); + final badMockResponse = MockResponse(); + + const htmlFetcherIdentifier = 'hor_geral.estudantes_view'; + const jsonFetcherIdentifier = 'mob_hor_geral.estudante'; - const htmlFetcherIdentifier = 'hor_geral.estudantes_view'; - const jsonFetcherIdentifier = 'mob_hor_geral.estudante'; - - Future testSchedule(WidgetTester tester) async { - final profile = Profile(); - profile.courses = [Course(id: 7474)]; + Future testSchedule(WidgetTester tester) async { + final profile = Profile(); + profile.courses = [Course(id: 7474)]; - NetworkRouter.httpClient = mockClient; - when(badMockResponse.statusCode).thenReturn(500); + NetworkRouter.httpClient = mockClient; + when(badMockResponse.statusCode).thenReturn(500); - final scheduleProvider = LectureProvider(); + final scheduleProvider = LectureProvider(); - const widget = SchedulePage(); + const widget = SchedulePage(); final providers = [ ChangeNotifierProvider(create: (_) => scheduleProvider), ChangeNotifierProvider(create: (_) => LastUserInfoProvider()), - ]; - - await tester.pumpWidget(testableWidget(widget, providers: providers)); - - const scheduleSlotTimeKey1 = 'schedule-slot-time-11:00-13:00'; - const scheduleSlotTimeKey2 = 'schedule-slot-time-14:00-16:00'; - - expect(find.byKey(const Key(scheduleSlotTimeKey1)), findsNothing); - expect(find.byKey(const Key(scheduleSlotTimeKey2)), findsNothing); - - final Completer completer = Completer(); - scheduleProvider.getUserLectures(completer, const Tuple2('', ''), Session(authenticated: true), profile); - await completer.future; - - 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'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('schedule-page-tab-0'))); - await tester.pumpAndSettle(); - - testScheduleSlot('ASSO', '11:00', '13:00', 'EaD', 'TP', 'DRP'); - - await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('schedule-page-tab-3'))); - await tester.pumpAndSettle(); - - testScheduleSlot('IOPE', '14:00', '16:00', 'EaD', 'TE', 'MTD'); - - } - - testWidgets('Schedule with JSON Fetcher', (WidgetTester tester) async { - NetworkRouter.httpClient = mockClient; - final mockJson = File('test/integration/resources/schedule_example.json') - .readAsStringSync(encoding: const Latin1Codec()); - when(mockResponse.body).thenReturn(mockJson); - when(mockResponse.statusCode).thenReturn(200); - when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => badMockResponse); - - when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => mockResponse); - - await testSchedule(tester); - }); - - testWidgets('Schedule with HTML Fetcher', (WidgetTester tester) async { - final mockHtml = File('test/integration/resources/schedule_example.html') - .readAsStringSync(encoding: const Latin1Codec()); - when(mockResponse.body).thenReturn(mockHtml); - when(mockResponse.statusCode).thenReturn(200); - when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => mockResponse); - - when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => badMockResponse); - - await testSchedule(tester); - }); - }); + ]; + + await tester.pumpWidget(testableWidget(widget, providers: providers)); + + const scheduleSlotTimeKey1 = 'schedule-slot-time-11:00-13:00'; + const scheduleSlotTimeKey2 = 'schedule-slot-time-14:00-16:00'; + + expect(find.byKey(const Key(scheduleSlotTimeKey1)), findsNothing); + expect(find.byKey(const Key(scheduleSlotTimeKey2)), findsNothing); + + final Completer completer = Completer(); + scheduleProvider.fetchUserLectures(completer, const Tuple2('', ''), + Session(authenticated: true), profile); + await completer.future; + + 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'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('schedule-page-tab-0'))); + await tester.pumpAndSettle(); + + testScheduleSlot('ASSO', '11:00', '13:00', 'EaD', 'TP', 'DRP'); + + await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('schedule-page-tab-3'))); + await tester.pumpAndSettle(); + + testScheduleSlot('IOPE', '14:00', '16:00', 'EaD', 'TE', 'MTD'); + } + + testWidgets('Schedule with JSON Fetcher', (WidgetTester tester) async { + NetworkRouter.httpClient = mockClient; + final mockJson = File('test/integration/resources/schedule_example.json') + .readAsStringSync(encoding: const Latin1Codec()); + when(mockResponse.body).thenReturn(mockJson); + when(mockResponse.statusCode).thenReturn(200); + when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), + headers: anyNamed('headers'))) + .thenAnswer((_) async => badMockResponse); + + when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), + headers: anyNamed('headers'))) + .thenAnswer((_) async => mockResponse); + + await testSchedule(tester); + }); + + testWidgets('Schedule with HTML Fetcher', (WidgetTester tester) async { + final mockHtml = File('test/integration/resources/schedule_example.html') + .readAsStringSync(encoding: const Latin1Codec()); + when(mockResponse.body).thenReturn(mockHtml); + when(mockResponse.statusCode).thenReturn(200); + when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), + headers: anyNamed('headers'))) + .thenAnswer((_) async => mockResponse); + + when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), + headers: anyNamed('headers'))) + .thenAnswer((_) async => badMockResponse); + + await testSchedule(tester); + }); + }); } diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 54fb87afc..ad0b4964a 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -1,6 +1,7 @@ // @dart=2.10 import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:tuple/tuple.dart'; @@ -10,7 +11,6 @@ import 'package:uni/model/entities/course_unit.dart'; 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/exam_provider.dart'; import 'package:uni/model/request_status.dart'; @@ -23,19 +23,25 @@ void main() { final mockResponse = MockResponse(); final sopeCourseUnit = CourseUnit( - abbreviation: 'SOPE', occurrId: 0, name: 'Sistemas Operativos', status: 'V'); + abbreviation: 'SOPE', + occurrId: 0, + name: 'Sistemas Operativos', + status: 'V'); final sdisCourseUnit = CourseUnit( - abbreviation: 'SDIS', occurrId: 0, name: 'Sistemas Distribuídos', status: 'V'); + abbreviation: 'SDIS', + occurrId: 0, + name: 'Sistemas Distribuídos', + status: 'V'); final List rooms = ['B119', 'B107', 'B205']; final DateTime beginSopeExam = DateTime.parse('2800-09-12 12:00'); final DateTime endSopeExam = DateTime.parse('2800-09-12 15:00'); - final sopeExam = Exam('1229', beginSopeExam, endSopeExam, 'SOPE', - rooms, 'Recurso - Época Recurso (2ºS)', 'feup'); + final sopeExam = Exam('1229', beginSopeExam, endSopeExam, 'SOPE', rooms, + 'Recurso - Época Recurso (2ºS)', 'feup'); final DateTime beginSdisExam = DateTime.parse('2800-09-12 12:00'); final DateTime endSdisExam = DateTime.parse('2800-09-12 15:00'); - final sdisExam = Exam('1230', beginSdisExam, endSdisExam, 'SDIS', - rooms, 'Recurso - Época Recurso (2ºS)', 'feup'); + final sdisExam = Exam('1230', beginSdisExam, endSdisExam, 'SDIS', rooms, + 'Recurso - Época Recurso (2ºS)', 'feup'); const Tuple2 userPersistentInfo = Tuple2('', ''); @@ -57,11 +63,12 @@ void main() { }); test('When given one exam', () async { - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {sopeExam}); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {sopeExam}); final action = Completer(); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -79,7 +86,7 @@ void main() { final Completer action = Completer(); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -94,19 +101,21 @@ void main() { since it is a Special Season Exam''', () async { final DateTime begin = DateTime.parse('2800-09-12 12:00'); final DateTime end = DateTime.parse('2800-09-12 15:00'); - final specialExam = Exam('1231', + final specialExam = Exam( + '1231', begin, end, 'SDIS', rooms, - 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', 'feup'); + 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', + 'feup'); final Completer action = Completer(); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam, specialExam}); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -122,7 +131,7 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => throw Exception('RIP')); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -135,14 +144,15 @@ void main() { test('When Exam is today in one hour', () async { final DateTime begin = DateTime.now().add(const Duration(hours: 1)); final DateTime end = DateTime.now().add(const Duration(hours: 2)); - final todayExam = Exam('1232',begin, end, 'SDIS', rooms, + final todayExam = Exam('1232', begin, end, 'SDIS', rooms, 'Recurso - Época Recurso (1ºS)', 'feup'); - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {todayExam}); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); final Completer action = Completer(); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -155,14 +165,15 @@ void main() { test('When Exam was one hour ago', () async { final DateTime end = DateTime.now().subtract(const Duration(hours: 1)); final DateTime begin = DateTime.now().subtract(const Duration(hours: 2)); - final todayExam = Exam('1233',begin, end, 'SDIS', rooms, + final todayExam = Exam('1233', begin, end, 'SDIS', rooms, 'Recurso - Época Recurso (1ºS)', 'feup'); - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {todayExam}); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); final Completer action = Completer(); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); @@ -175,14 +186,15 @@ void main() { test('When Exam is ocurring', () async { final DateTime before = DateTime.now().subtract(const Duration(hours: 1)); final DateTime after = DateTime.now().add(const Duration(hours: 1)); - final todayExam = Exam('1234',before, after, 'SDIS', rooms, - 'Recurso - Época Recurso (1ºS)','feup'); + final todayExam = Exam('1234', before, after, 'SDIS', rooms, + 'Recurso - Época Recurso (1ºS)', 'feup'); - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {todayExam}); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); final Completer action = Completer(); - provider.getUserExams( + provider.fetchUserExams( action, parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.busy); diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 72611fec1..e9dcee38f 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -1,6 +1,7 @@ // @dart=2.10 import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:tuple/tuple.dart'; @@ -9,7 +10,6 @@ import 'package:uni/model/entities/course.dart'; 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/lecture_provider.dart'; import 'package:uni/model/request_status.dart'; @@ -48,7 +48,7 @@ void main() { when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => [lecture1, lecture2]); - provider.getUserLectures(action, userPersistentInfo, session, profile, + provider.fetchUserLectures(action, userPersistentInfo, session, profile, fetcher: fetcherMock); expect(provider.status, RequestStatus.busy); @@ -64,7 +64,7 @@ void main() { when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => throw Exception('💥')); - provider.getUserLectures(action, userPersistentInfo, session, profile); + provider.fetchUserLectures(action, userPersistentInfo, session, profile); expect(provider.status, RequestStatus.busy); await action.future; From 4842265d0e6832dedee14dc99349d1aa63aa8a98 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 02:30:17 +0100 Subject: [PATCH 032/100] Combine favorite cards and home page editing providers --- uni/lib/main.dart | 11 +--- .../home_page_editing_mode_provider.dart | 25 -------- ..._provider.dart => home_page_provider.dart} | 18 +++++- uni/lib/model/providers/profile_provider.dart | 22 ++++--- .../providers/state_provider_notifier.dart | 2 + uni/lib/model/providers/state_providers.dart | 18 ++---- .../bus_stop_next_arrivals.dart | 2 +- .../widgets/estimated_arrival_timestamp.dart | 2 +- .../bus_stop_selection.dart | 2 +- uni/lib/view/calendar/calendar.dart | 2 +- .../common_widgets/last_update_timestamp.dart | 2 +- .../request_dependent_widget_builder.dart | 2 +- uni/lib/view/course_units/course_units.dart | 3 +- uni/lib/view/exams/exams.dart | 2 +- uni/lib/view/home/widgets/bus_stop_card.dart | 2 +- uni/lib/view/home/widgets/exam_card.dart | 2 +- .../view/home/widgets/main_cards_list.dart | 61 +++++++++---------- .../view/home/widgets/restaurant_card.dart | 21 +++---- uni/lib/view/home/widgets/schedule_card.dart | 2 +- uni/lib/view/lazy_consumer.dart | 8 +-- uni/lib/view/library/library.dart | 17 +----- .../widgets/library_occupation_card.dart | 2 +- uni/lib/view/locations/locations.dart | 2 +- uni/lib/view/login/login.dart | 2 +- uni/lib/view/profile/profile.dart | 2 +- .../profile/widgets/account_info_card.dart | 2 +- .../view/profile/widgets/print_info_card.dart | 2 +- .../profile/widgets/profile_overview.dart | 2 +- .../view/restaurant/restaurant_page_view.dart | 2 +- uni/lib/view/schedule/schedule.dart | 2 +- 30 files changed, 104 insertions(+), 140 deletions(-) delete mode 100644 uni/lib/model/providers/home_page_editing_mode_provider.dart rename uni/lib/model/providers/{favorite_cards_provider.dart => home_page_provider.dart} (67%) diff --git a/uni/lib/main.dart b/uni/lib/main.dart index aea2edfae..1a61266e0 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -12,8 +12,7 @@ import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; -import 'package:uni/model/providers/favorite_cards_provider.dart'; -import 'package:uni/model/providers/home_page_editing_mode_provider.dart'; +import 'package:uni/model/providers/home_page_provider.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; @@ -58,8 +57,7 @@ Future main() async { LibraryOccupationProvider(), FacultyLocationsProvider(), LastUserInfoProvider(), - FavoriteCardsProvider(), - HomePageEditingModeProvider()); + HomePageProvider()); OnStartUp.onStart(stateProviders.sessionProvider); WidgetsFlutterBinding.ensureInitialized(); @@ -100,10 +98,7 @@ Future main() async { ChangeNotifierProvider( create: (context) => stateProviders.lastUserInfoProvider), ChangeNotifierProvider( - create: (context) => - stateProviders.favoriteCardsProvider), - ChangeNotifierProvider( - create: (context) => stateProviders.homePageEditingMode), + create: (context) => stateProviders.homePageProvider), ], child: ChangeNotifierProvider( create: (_) => ThemeNotifier(savedTheme), diff --git a/uni/lib/model/providers/home_page_editing_mode_provider.dart b/uni/lib/model/providers/home_page_editing_mode_provider.dart deleted file mode 100644 index e45457874..000000000 --- a/uni/lib/model/providers/home_page_editing_mode_provider.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/state_provider_notifier.dart'; - -class HomePageEditingModeProvider extends StateProviderNotifier { - bool _isEditing = false; - - bool get isEditing => _isEditing; - - @override - void loadFromStorage() {} - - @override - Future loadFromRemote(Session session, Profile profile) async {} - - setHomePageEditingMode(bool state) { - _isEditing = state; - notifyListeners(); - } - - toggle() { - _isEditing = !_isEditing; - notifyListeners(); - } -} diff --git a/uni/lib/model/providers/favorite_cards_provider.dart b/uni/lib/model/providers/home_page_provider.dart similarity index 67% rename from uni/lib/model/providers/favorite_cards_provider.dart rename to uni/lib/model/providers/home_page_provider.dart index 79be2a624..8fe189b80 100644 --- a/uni/lib/model/providers/favorite_cards_provider.dart +++ b/uni/lib/model/providers/home_page_provider.dart @@ -1,22 +1,34 @@ -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/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/utils/favorite_widget_type.dart'; -class FavoriteCardsProvider extends StateProviderNotifier { +class HomePageProvider extends StateProviderNotifier { List _favoriteCards = []; + bool _isEditing = false; List get favoriteCards => _favoriteCards.toList(); + bool get isEditing => _isEditing; @override - loadFromStorage() async { + Future loadFromStorage() async { setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); } @override Future loadFromRemote(Session session, Profile profile) async {} + setHomePageEditingMode(bool state) { + _isEditing = state; + notifyListeners(); + } + + toggleHomePageEditingMode() { + _isEditing = !_isEditing; + notifyListeners(); + } + setFavoriteCards(List favoriteCards) { _favoriteCards = favoriteCards; notifyListeners(); diff --git a/uni/lib/model/providers/profile_provider.dart b/uni/lib/model/providers/profile_provider.dart index 35bb27a9f..4ab96e5b7 100644 --- a/uni/lib/model/providers/profile_provider.dart +++ b/uni/lib/model/providers/profile_provider.dart @@ -67,7 +67,7 @@ class ProfileProvider extends StateProviderNotifier { void loadBalanceRefreshTimes() async { final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); final Map refreshTimes = - await refreshTimesDb.refreshTimes(); + await refreshTimesDb.refreshTimes(); final printRefreshTime = refreshTimes['print']; final feesRefreshTime = refreshTimes['fees']; @@ -93,7 +93,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); @@ -122,7 +122,7 @@ class ProfileProvider extends StateProviderNotifier { Future storeRefreshTime(String db, String currentTime) async { final AppRefreshTimesDatabase refreshTimesDatabase = - AppRefreshTimesDatabase(); + AppRefreshTimesDatabase(); refreshTimesDatabase.saveRefreshTime(db, currentTime); } @@ -133,7 +133,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); @@ -161,6 +161,7 @@ class ProfileProvider extends StateProviderNotifier { } fetchUserInfo(Completer action, Session session) async { + print("fetched user info"); try { updateStatus(RequestStatus.busy); @@ -168,6 +169,8 @@ class ProfileProvider extends StateProviderNotifier { _profile = res; }); + print("profile courses: ${_profile.courses}"); + final ucs = CurrentCourseUnitsFetcher() .getCurrentCourseUnits(session) .then((res) => _profile.currentCourseUnits = res); @@ -176,7 +179,7 @@ class ProfileProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final profileDb = AppUserDataDatabase(); profileDb.insertUserData(_profile); @@ -189,8 +192,8 @@ class ProfileProvider extends StateProviderNotifier { action.complete(); } - fetchCourseUnitsAndCourseAverages( - Session session, Completer action) async { + fetchCourseUnitsAndCourseAverages(Session session, + Completer action) async { updateStatus(RequestStatus.busy); try { final List courses = profile.courses; @@ -199,8 +202,11 @@ class ProfileProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); notifyListeners(); + print("ola"); + print(_profile.currentCourseUnits); + final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); await coursesDb.saveNewCourses(courses); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index b818caddc..265a7ad52 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -23,6 +23,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { _initialized = true; + updateStatus(RequestStatus.busy); + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); final sessionIsPersistent = diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index f8a83dcbc..af0287b06 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -4,8 +4,7 @@ import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; -import 'package:uni/model/providers/favorite_cards_provider.dart'; -import 'package:uni/model/providers/home_page_editing_mode_provider.dart'; +import 'package:uni/model/providers/home_page_provider.dart'; import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; @@ -24,8 +23,7 @@ class StateProviders { final LibraryOccupationProvider libraryOccupationProvider; final FacultyLocationsProvider facultyLocationsProvider; final LastUserInfoProvider lastUserInfoProvider; - final FavoriteCardsProvider favoriteCardsProvider; - final HomePageEditingModeProvider homePageEditingMode; + final HomePageProvider homePageProvider; StateProviders( this.lectureProvider, @@ -38,8 +36,7 @@ class StateProviders { this.libraryOccupationProvider, this.facultyLocationsProvider, this.lastUserInfoProvider, - this.favoriteCardsProvider, - this.homePageEditingMode); + this.homePageProvider); static StateProviders fromContext(BuildContext context) { final lectureProvider = @@ -61,10 +58,8 @@ class StateProviders { Provider.of(context, listen: false); final lastUserInfoProvider = Provider.of(context, listen: false); - final favoriteCardsProvider = - Provider.of(context, listen: false); - final homePageEditingMode = - Provider.of(context, listen: false); + final homePageProvider = + Provider.of(context, listen: false); return StateProviders( lectureProvider, @@ -77,7 +72,6 @@ class StateProviders { libraryOccupationProvider, facultyLocationsProvider, lastUserInfoProvider, - favoriteCardsProvider, - homePageEditingMode); + homePageProvider); } } diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index e99d2d6f0..ad2c15092 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -22,7 +22,7 @@ class BusStopNextArrivalsPageState @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, busProvider, _) => ListView(children: [ + builder: (context, busProvider) => ListView(children: [ NextArrivals(busProvider.configuredBusStops, busProvider.status) ])); } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index 0fa7c3cdf..a468aa2ad 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart @@ -14,7 +14,7 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, busProvider, _) => + builder: (context, busProvider) => getContent(context, busProvider.timeStamp), ); } diff --git a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart index a273c830c..08eb2dd8a 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -36,7 +36,7 @@ class BusStopSelectionPageState @override Widget getBody(BuildContext context) { final width = MediaQuery.of(context).size.width; - return LazyConsumer(builder: (context, busProvider, _) { + return LazyConsumer(builder: (context, busProvider) { final List rows = []; busProvider.configuredBusStops.forEach((stopCode, stopData) => rows.add(BusStopSelectionRow(stopCode, stopData))); diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 2af4205c1..181ec4036 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -17,7 +17,7 @@ class CalendarPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, calendarProvider, _) => + builder: (context, calendarProvider) => getCalendarPage(context, calendarProvider.calendar), ); } diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 39f7fec46..a996352c1 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -34,7 +34,7 @@ class _LastUpdateTimeStampState extends State { @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, lastUserInfoProvider, _) => Container( + builder: (context, lastUserInfoProvider) => Container( padding: const EdgeInsets.only(top: 8.0, bottom: 10.0), child: _getContent(context, lastUserInfoProvider.lastUpdateTime!)), ); diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index a721917c8..edf31fa4f 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -36,7 +36,7 @@ class RequestDependentWidgetBuilder extends StatelessWidget { @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, lastUserInfoProvider, _) { + builder: (context, lastUserInfoProvider) { switch (status) { case RequestStatus.successful: case RequestStatus.none: diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 678e79ea5..e815bcd1b 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -28,8 +28,7 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, profileProvider, _) { + return LazyConsumer(builder: (context, profileProvider) { final List courseUnits = profileProvider.profile.currentCourseUnits; List availableYears = []; diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 2d3420ddc..5988080be 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -22,7 +22,7 @@ class ExamsPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer(builder: (context, examProvider, _) { + return LazyConsumer(builder: (context, examProvider) { return ListView( children: [ Column( diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 9f778f350..d5a12acb1 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -25,7 +25,7 @@ class BusStopCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, busProvider, _) { + builder: (context, busProvider) { return getCardContent( context, busProvider.configuredBusStops, busProvider.status); }, diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 29e55ff4a..d00e962d0 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -32,7 +32,7 @@ class ExamCard extends GenericCard { /// that no exams exist is displayed. @override Widget buildCardContent(BuildContext context) { - return LazyConsumer(builder: (context, examProvider, _) { + return LazyConsumer(builder: (context, examProvider) { final filteredExams = examProvider.getFilteredExams(); final hiddenExams = examProvider.hiddenExams; final List exams = filteredExams diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 857a55140..11510c610 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/model/providers/favorite_cards_provider.dart'; -import 'package:uni/model/providers/home_page_editing_mode_provider.dart'; +import 'package:uni/model/providers/home_page_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/utils/drawer_items.dart'; import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; -import 'package:uni/view/library/widgets/library_occupation_card.dart'; -import 'package:uni/view/profile/widgets/account_info_card.dart'; -import 'package:uni/view/home/widgets/exit_app_dialog.dart'; +import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/home/widgets/bus_stop_card.dart'; import 'package:uni/view/home/widgets/exam_card.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/home/widgets/exit_app_dialog.dart'; import 'package:uni/view/home/widgets/schedule_card.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/library/widgets/library_occupation_card.dart'; +import 'package:uni/view/profile/widgets/account_info_card.dart'; typedef CardCreator = GenericCard Function( Key key, bool isEditingMode, dynamic Function()? onDelete); @@ -41,37 +41,36 @@ class MainCardsList extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, editingModeProvider, favoriteCardsProvider, _) => - Scaffold( + return LazyConsumer( + builder: (context, homePageProvider) => Scaffold( body: BackButtonExitWrapper( context: context, child: SizedBox( height: MediaQuery.of(context).size.height, - child: editingModeProvider.isEditing + child: homePageProvider.isEditing ? ReorderableListView( onReorder: (oldIndex, newIndex) => reorderCard( oldIndex, newIndex, - favoriteCardsProvider.favoriteCards, + homePageProvider.favoriteCards, context), - header: createTopBar(context, editingModeProvider), + header: createTopBar(context, homePageProvider), children: favoriteCardsFromTypes( - favoriteCardsProvider.favoriteCards, + homePageProvider.favoriteCards, context, - editingModeProvider), + homePageProvider), ) : ListView( children: [ - createTopBar(context, editingModeProvider), + createTopBar(context, homePageProvider), ...favoriteCardsFromTypes( - favoriteCardsProvider.favoriteCards, + homePageProvider.favoriteCards, context, - editingModeProvider) + homePageProvider) ], )), ), - floatingActionButton: editingModeProvider.isEditing + floatingActionButton: homePageProvider.isEditing ? createActionButton(context) : null, )); @@ -106,8 +105,7 @@ class MainCardsList extends StatelessWidget { List getCardAdders(BuildContext context) { final userSession = Provider.of(context, listen: false); final List favorites = - Provider.of(context, listen: false) - .favoriteCards; + Provider.of(context, listen: false).favoriteCards; final possibleCardAdditions = cardCreators.entries .where((e) => e.key.isVisible(userSession.faculties)) @@ -136,16 +134,15 @@ class MainCardsList extends StatelessWidget { } Widget createTopBar( - BuildContext context, HomePageEditingModeProvider editingModeProvider) { + BuildContext context, HomePageProvider editingModeProvider) { return Container( padding: const EdgeInsets.fromLTRB(20, 20, 20, 5), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ PageTitle( name: DrawerItem.navPersonalArea.title, center: false, pad: false), GestureDetector( - onTap: () => - Provider.of(context, listen: false) - .setHomePageEditingMode(!editingModeProvider.isEditing), + onTap: () => Provider.of(context, listen: false) + .setHomePageEditingMode(!editingModeProvider.isEditing), child: Text( editingModeProvider.isEditing ? 'Concluir Edição' : 'Editar', style: Theme.of(context).textTheme.bodySmall)) @@ -154,7 +151,7 @@ class MainCardsList extends StatelessWidget { } List favoriteCardsFromTypes(List cardTypes, - BuildContext context, HomePageEditingModeProvider editingModeProvider) { + BuildContext context, HomePageProvider editingModeProvider) { final userSession = Provider.of(context, listen: false).session; return cardTypes @@ -162,7 +159,9 @@ class MainCardsList extends StatelessWidget { .where((type) => cardCreators.containsKey(type)) .map((type) { final i = cardTypes.indexOf(type); - return cardCreators[type]!(Key(i.toString()), editingModeProvider.isEditing, + return cardCreators[type]!( + Key(i.toString()), + editingModeProvider.isEditing, () => removeCardIndexFromFavorites(i, context)); }).toList(); } @@ -177,16 +176,14 @@ class MainCardsList extends StatelessWidget { void removeCardIndexFromFavorites(int i, BuildContext context) { final List favorites = - Provider.of(context, listen: false) - .favoriteCards; + Provider.of(context, listen: false).favoriteCards; favorites.removeAt(i); saveFavoriteCards(context, favorites); } void addCardToFavorites(FavoriteWidgetType type, BuildContext context) { final List favorites = - Provider.of(context, listen: false) - .favoriteCards; + Provider.of(context, listen: false).favoriteCards; if (!favorites.contains(type)) { favorites.add(type); } @@ -195,7 +192,7 @@ class MainCardsList extends StatelessWidget { void saveFavoriteCards( BuildContext context, List favorites) { - Provider.of(context, listen: false) + Provider.of(context, listen: false) .setFavoriteCards(favorites); AppSharedPreferences.saveFavoriteCards(favorites); } diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 3c8f7f8c8..0f7c248dc 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -23,17 +23,16 @@ class RestaurantCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, restaurantProvider, _) => - RequestDependentWidgetBuilder( - context: context, - status: restaurantProvider.status, - contentGenerator: generateRestaurant, - content: restaurantProvider.restaurants, - contentChecker: restaurantProvider.restaurants.isNotEmpty, - onNullContent: Center( - child: Text('Não existem cantinas para apresentar', - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center)))); + builder: (context, restaurantProvider) => RequestDependentWidgetBuilder( + context: context, + status: restaurantProvider.status, + contentGenerator: generateRestaurant, + content: restaurantProvider.restaurants, + contentChecker: restaurantProvider.restaurants.isNotEmpty, + onNullContent: Center( + child: Text('Não existem cantinas para apresentar', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center)))); } Widget generateRestaurant(canteens, context) { diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index 5f6f13ba9..cf4ff2214 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -24,7 +24,7 @@ class ScheduleCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, lectureProvider, _) => RequestDependentWidgetBuilder( + builder: (context, lectureProvider) => RequestDependentWidgetBuilder( context: context, status: lectureProvider.status, contentGenerator: generateSchedule, diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 0027c7522..42b4f10d6 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -5,7 +5,7 @@ import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; class LazyConsumer extends StatelessWidget { - final Widget Function(BuildContext, T, Widget?) builder; + final Widget Function(BuildContext, T) builder; const LazyConsumer({ Key? key, @@ -23,8 +23,8 @@ class LazyConsumer extends StatelessWidget { .ensureInitialized(session, profile); }); - return Consumer( - builder: builder, - ); + return Consumer(builder: (context, provider, _) { + return builder(context, provider); + }); } } diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 2756dbc51..eb2f3bfc4 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -18,23 +18,8 @@ class LibraryPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, libraryOccupationProvider, _) => + builder: (context, libraryOccupationProvider) => LibraryPage(libraryOccupationProvider.occupation)); - -/* - return StoreConnector>( - converter: (store) { - final LibraryOccupation? occupation = - store.state.content['libraryOccupation']; - return Tuple2(occupation, store.state.content['libraryOccupationStatus']); - }, builder: (context, occupationInfo) { - if (occupationInfo.item2 == RequestStatus.busy) { - return const Center(child: CircularProgressIndicator()); - } else { - return LibraryPage(occupationInfo.item1); - } - }); - */ } } diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index 70da1215f..be02eee33 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -25,7 +25,7 @@ class LibraryOccupationCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, libraryOccupationProvider, _) => + builder: (context, libraryOccupationProvider) => RequestDependentWidgetBuilder( context: context, status: libraryOccupationProvider.status, diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index 2b2a573e8..5c7c0e7de 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -29,7 +29,7 @@ class LocationsPageState extends GeneralPageViewState @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, locationsProvider, _) { + builder: (context, locationsProvider) { return LocationsPageView( locations: locationsProvider.locations, status: locationsProvider.status); diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index ead902fa0..738ee8ba2 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -223,7 +223,7 @@ class LoginPageViewState extends State { /// Creates a widget for the user login depending on the status of his login. Widget createStatusWidget(BuildContext context) { return LazyConsumer( - builder: (context, sessionProvider, _) { + builder: (context, sessionProvider) { switch (sessionProvider.status) { case RequestStatus.busy: return const SizedBox( diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 1c988249e..656477735 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -19,7 +19,7 @@ class ProfilePageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, profileStateProvider, _) { + builder: (context, profileStateProvider) { final profile = profileStateProvider.profile; final List courseWidgets = profile.courses .map((e) => [ diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 4ffbcbbee..c759692b9 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -16,7 +16,7 @@ class AccountInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, profileStateProvider, _) { + builder: (context, profileStateProvider) { final profile = profileStateProvider.profile; return Column(children: [ Table( diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index 264a85689..0ae056c15 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -14,7 +14,7 @@ class PrintInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, profileStateProvider, _) { + builder: (context, profileStateProvider) { final profile = profileStateProvider.profile; return Column( mainAxisSize: MainAxisSize.min, diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index b1593144b..ad3538caf 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -19,7 +19,7 @@ class ProfileOverview extends StatelessWidget { @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, sessionProvider, _) { + builder: (context, sessionProvider) { return FutureBuilder( future: loadProfilePicture(sessionProvider.session), builder: (BuildContext context, AsyncSnapshot profilePic) => diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 9b7f02144..852b6b1ab 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -36,7 +36,7 @@ class _RestaurantPageState extends GeneralPageViewState @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, restaurantProvider, _) { + builder: (context, restaurantProvider) { return Column(children: [ ListView(scrollDirection: Axis.vertical, shrinkWrap: true, children: [ Container( diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 971c45eed..7943f89d3 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -21,7 +21,7 @@ class SchedulePageState extends State { @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, lectureProvider, _) { + builder: (context, lectureProvider) { return SchedulePageView( lectures: lectureProvider.lectures, scheduleStatus: lectureProvider.status, From 1beb06985e3f167826f529b92e08992412f790f2 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 16:36:40 +0100 Subject: [PATCH 033/100] Fix course units loading --- .../background_workers/notifications.dart | 2 +- .../fetchers/all_course_units_fetcher.dart | 3 + uni/lib/model/entities/profile.dart | 19 +++-- .../model/providers/bus_stop_provider.dart | 11 ++- .../model/providers/calendar_provider.dart | 3 +- uni/lib/model/providers/exam_provider.dart | 11 ++- .../providers/faculty_locations_provider.dart | 2 +- .../providers/last_user_info_provider.dart | 2 +- uni/lib/model/providers/lecture_provider.dart | 3 +- .../library_occupation_provider.dart | 4 +- uni/lib/model/providers/profile_provider.dart | 73 +++++++++---------- uni/lib/model/providers/session_provider.dart | 16 ++-- .../providers/state_provider_notifier.dart | 10 +-- uni/lib/view/course_units/course_units.dart | 5 +- uni/lib/view/lazy_consumer.dart | 20 +++-- uni/lib/view/login/login.dart | 4 +- .../profile/widgets/profile_overview.dart | 6 +- uni/lib/view/splash/splash.dart | 2 +- 18 files changed, 97 insertions(+), 99 deletions(-) diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index a270fa644..00d0fdef2 100644 --- a/uni/lib/controller/background_workers/notifications.dart +++ b/uni/lib/controller/background_workers/notifications.dart @@ -83,7 +83,7 @@ class NotificationManager { } void initializeNotifications() async { - //guarentees that the execution is only done once in the lifetime of the app. + // guarantees that the execution is only done once in the lifetime of the app. if (_initialized) return; _initialized = true; _initFlutterNotificationsPlugin(); diff --git a/uni/lib/controller/fetchers/all_course_units_fetcher.dart b/uni/lib/controller/fetchers/all_course_units_fetcher.dart index baa454ebf..5d876b1d7 100644 --- a/uni/lib/controller/fetchers/all_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/all_course_units_fetcher.dart @@ -10,11 +10,14 @@ class AllCourseUnitsFetcher { List courses, Session session) async { final List allCourseUnits = []; for (var course in courses) { + print("course: ${course.name}"); try { final List courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( course, session); + print("courseUnits: ${courseUnits.length}"); allCourseUnits.addAll(courseUnits.where((c) => c.enrollmentIsValid())); + print("allCourseUnits: ${allCourseUnits.length}"); } catch (e) { Logger().e('Failed to fetch course units for ${course.name}', e); } diff --git a/uni/lib/model/entities/profile.dart b/uni/lib/model/entities/profile.dart index b5fac913d..c2028a5f7 100644 --- a/uni/lib/model/entities/profile.dart +++ b/uni/lib/model/entities/profile.dart @@ -11,18 +11,17 @@ class Profile { final String printBalance; final String feesBalance; final String feesLimit; - late List courses; - late List currentCourseUnits; + List courses; + List courseUnits; - Profile( - {this.name = '', - this.email = '', - courses, - this.printBalance = '', - this.feesBalance = '', - this.feesLimit = ''}) + Profile({this.name = '', + this.email = '', + courses, + this.printBalance = '', + this.feesBalance = '', + this.feesLimit = ''}) : courses = courses ?? [], - currentCourseUnits = []; + courseUnits = []; /// Creates a new instance from a JSON object. static Profile fromResponse(dynamic response) { diff --git a/uni/lib/model/providers/bus_stop_provider.dart b/uni/lib/model/providers/bus_stop_provider.dart index 302347b64..f4b126fba 100644 --- a/uni/lib/model/providers/bus_stop_provider.dart +++ b/uni/lib/model/providers/bus_stop_provider.dart @@ -21,18 +21,17 @@ class BusStopProvider extends StateProviderNotifier { DateTime get timeStamp => _timeStamp; @override - void loadFromStorage() async { + Future loadFromStorage() async { final AppBusStopDatabase busStopsDb = AppBusStopDatabase(); final Map stops = await busStopsDb.busStops(); - _configuredBusStops = stops; - notifyListeners(); - getUserBusTrips(Completer()); } @override - void loadFromRemote(Session session, Profile profile) { - getUserBusTrips(Completer()); + Future loadFromRemote(Session session, Profile profile) async { + final action = Completer(); + getUserBusTrips(action); + await action.future; } getUserBusTrips(Completer action) async { diff --git a/uni/lib/model/providers/calendar_provider.dart b/uni/lib/model/providers/calendar_provider.dart index 281e1b743..add1e8a07 100644 --- a/uni/lib/model/providers/calendar_provider.dart +++ b/uni/lib/model/providers/calendar_provider.dart @@ -41,9 +41,8 @@ class CalendarProvider extends StateProviderNotifier { } @override - void loadFromStorage() async { + Future loadFromStorage() async { final CalendarDatabase db = CalendarDatabase(); _calendar = await db.calendar(); - notifyListeners(); } } diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/exam_provider.dart index 1192abfdb..0f304f6f8 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/exam_provider.dart @@ -28,26 +28,25 @@ class ExamProvider extends StateProviderNotifier { UnmodifiableMapView(_filteredExamsTypes); @override - void loadFromStorage() async { + Future loadFromStorage() async { setFilteredExams( await AppSharedPreferences.getFilteredExams(), Completer()); setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); final AppExamsDatabase db = AppExamsDatabase(); - final List exs = await db.exams(); - _exams = exs; - notifyListeners(); + final List exams = await db.exams(); + _exams = exams; } @override - void loadFromRemote(Session session, Profile profile) async { + Future loadFromRemote(Session session, Profile profile) async { final Completer action = Completer(); final ParserExams parserExams = ParserExams(); final Tuple2 userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); fetchUserExams(action, parserExams, userPersistentInfo, profile, session, - profile.currentCourseUnits); + profile.courseUnits); await action.future; } diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart index 0e1af6403..e02d7905a 100644 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ b/uni/lib/model/providers/faculty_locations_provider.dart @@ -14,7 +14,7 @@ class FacultyLocationsProvider extends StateProviderNotifier { UnmodifiableListView(_locations); @override - void loadFromStorage() async { + Future loadFromStorage() async { _locations = await LocationFetcherAsset().getLocations(); } diff --git a/uni/lib/model/providers/last_user_info_provider.dart b/uni/lib/model/providers/last_user_info_provider.dart index 724091265..fb9468c89 100644 --- a/uni/lib/model/providers/last_user_info_provider.dart +++ b/uni/lib/model/providers/last_user_info_provider.dart @@ -19,7 +19,7 @@ class LastUserInfoProvider extends StateProviderNotifier { } @override - void loadFromStorage() async { + Future loadFromStorage() async { final AppLastUserInfoUpdateDatabase db = AppLastUserInfoUpdateDatabase(); _lastUpdateTime = await db.getLastUserInfoUpdateTime(); notifyListeners(); diff --git a/uni/lib/model/providers/lecture_provider.dart b/uni/lib/model/providers/lecture_provider.dart index d1e26fa96..177a4bb61 100644 --- a/uni/lib/model/providers/lecture_provider.dart +++ b/uni/lib/model/providers/lecture_provider.dart @@ -20,11 +20,10 @@ class LectureProvider extends StateProviderNotifier { UnmodifiableListView get lectures => UnmodifiableListView(_lectures); @override - void loadFromStorage() async { + Future loadFromStorage() async { final AppLecturesDatabase db = AppLecturesDatabase(); final List lectures = await db.lectures(); _lectures = lectures; - notifyListeners(); } @override diff --git a/uni/lib/model/providers/library_occupation_provider.dart b/uni/lib/model/providers/library_occupation_provider.dart index 31b8092a1..6e0f58fb0 100644 --- a/uni/lib/model/providers/library_occupation_provider.dart +++ b/uni/lib/model/providers/library_occupation_provider.dart @@ -15,12 +15,10 @@ class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupation? get occupation => _occupation; @override - void loadFromStorage() async { + Future loadFromStorage() async { final LibraryOccupationDatabase db = LibraryOccupationDatabase(); final LibraryOccupation occupation = await db.occupation(); - _occupation = occupation; - notifyListeners(); } @override diff --git a/uni/lib/model/providers/profile_provider.dart b/uni/lib/model/providers/profile_provider.dart index 4ab96e5b7..456dcd781 100644 --- a/uni/lib/model/providers/profile_provider.dart +++ b/uni/lib/model/providers/profile_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; +import 'package:uni/controller/fetchers/all_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/current_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/fees_fetcher.dart'; import 'package:uni/controller/fetchers/print_fetcher.dart'; @@ -14,14 +15,12 @@ import 'package:uni/controller/local_storage/app_user_database.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; import 'package:uni/model/entities/course.dart'; +import 'package:uni/model/entities/course_unit.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'; -// ignore: always_use_package_imports -import '../../controller/fetchers/all_course_units_fetcher.dart'; - class ProfileProvider extends StateProviderNotifier { Profile _profile = Profile(); DateTime? _feesRefreshTime; @@ -34,14 +33,17 @@ class ProfileProvider extends StateProviderNotifier { Profile get profile => _profile; @override - void loadFromStorage() async { - loadCourses(); - loadBalanceRefreshTimes(); - loadCourseUnits(); + Future loadFromStorage() async { + await Future.wait( + [loadCourses(), loadBalanceRefreshTimes(), loadCourseUnits()]); } @override Future loadFromRemote(Session session, Profile profile) async { + final userInfoAction = Completer(); + fetchUserInfo(userInfoAction, session); + await userInfoAction.future; + final Completer userFeesAction = Completer(); fetchUserFees(userFeesAction, session); @@ -51,10 +53,14 @@ class ProfileProvider extends StateProviderNotifier { final Completer courseUnitsAction = Completer(); fetchCourseUnitsAndCourseAverages(session, courseUnitsAction); - await Future.wait([userFeesAction.future, printBalanceAction.future]); + await Future.wait([ + userFeesAction.future, + printBalanceAction.future, + courseUnitsAction.future + ]); } - void loadCourses() async { + Future loadCourses() async { final profileDb = AppUserDataDatabase(); _profile = await profileDb.getUserData(); @@ -64,10 +70,10 @@ class ProfileProvider extends StateProviderNotifier { _profile.courses = courses; } - void loadBalanceRefreshTimes() async { + Future loadBalanceRefreshTimes() async { final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); final Map refreshTimes = - await refreshTimesDb.refreshTimes(); + await refreshTimesDb.refreshTimes(); final printRefreshTime = refreshTimes['print']; final feesRefreshTime = refreshTimes['fees']; @@ -79,9 +85,9 @@ class ProfileProvider extends StateProviderNotifier { } } - void loadCourseUnits() async { + Future loadCourseUnits() async { final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); - profile.currentCourseUnits = await db.courseUnits(); + profile.courseUnits = await db.courseUnits(); } fetchUserFees(Completer action, Session session) async { @@ -93,7 +99,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); @@ -122,7 +128,7 @@ class ProfileProvider extends StateProviderNotifier { Future storeRefreshTime(String db, String currentTime) async { final AppRefreshTimesDatabase refreshTimesDatabase = - AppRefreshTimesDatabase(); + AppRefreshTimesDatabase(); refreshTimesDatabase.saveRefreshTime(db, currentTime); } @@ -133,7 +139,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); @@ -161,25 +167,20 @@ class ProfileProvider extends StateProviderNotifier { } fetchUserInfo(Completer action, Session session) async { - print("fetched user info"); try { updateStatus(RequestStatus.busy); - final profile = ProfileFetcher.getProfile(session).then((res) { - _profile = res; - }); + final profile = await ProfileFetcher.getProfile(session); + final currentCourseUnits = + await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); - print("profile courses: ${_profile.courses}"); + _profile = profile; + _profile.courseUnits = currentCourseUnits; - final ucs = CurrentCourseUnitsFetcher() - .getCurrentCourseUnits(session) - .then((res) => _profile.currentCourseUnits = res); - await Future.wait([profile, ucs]); - notifyListeners(); updateStatus(RequestStatus.successful); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final profileDb = AppUserDataDatabase(); profileDb.insertUserData(_profile); @@ -192,28 +193,24 @@ class ProfileProvider extends StateProviderNotifier { action.complete(); } - fetchCourseUnitsAndCourseAverages(Session session, - Completer action) async { + fetchCourseUnitsAndCourseAverages( + Session session, Completer action) async { updateStatus(RequestStatus.busy); try { final List courses = profile.courses; - _profile.currentCourseUnits = await AllCourseUnitsFetcher() - .getAllCourseUnitsAndCourseAverages(courses, session); - updateStatus(RequestStatus.successful); - notifyListeners(); + final List allCourseUnits = await AllCourseUnitsFetcher() + .getAllCourseUnitsAndCourseAverages(profile.courses, session); - print("ola"); - print(_profile.currentCourseUnits); + _profile.courseUnits = allCourseUnits; final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); await coursesDb.saveNewCourses(courses); final courseUnitsDatabase = AppCourseUnitsDatabase(); - await courseUnitsDatabase - .saveNewCourseUnits(_profile.currentCourseUnits); + await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); } } catch (e) { Logger().e('Failed to get all user ucs: $e'); diff --git a/uni/lib/model/providers/session_provider.dart b/uni/lib/model/providers/session_provider.dart index 5ef686b5d..fe334db14 100644 --- a/uni/lib/model/providers/session_provider.dart +++ b/uni/lib/model/providers/session_provider.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; import 'package:uni/controller/background_workers/notifications.dart'; -import 'package:uni/controller/load_info.dart'; import 'package:uni/controller/load_static/terms_and_conditions.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/networking/network_router.dart'; @@ -24,13 +23,12 @@ class SessionProvider extends StateProviderNotifier { UnmodifiableListView(_faculties); @override - void loadFromStorage() {} + Future loadFromStorage() async {} @override Future loadFromRemote(Session session, Profile profile) async {} - login( - Completer action, + login(Completer action, String username, String password, List faculties, @@ -51,10 +49,10 @@ class SessionProvider extends StateProviderNotifier { username, password, faculties); } Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); //loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); - await loadUserProfileInfoFromRemote(stateProviders); + //await loadUserProfileInfoFromRemote(stateProviders); usernameController.clear(); passwordController.clear(); @@ -63,7 +61,7 @@ class SessionProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } else { final String responseHtml = - await NetworkRouter.loginInSigarra(username, password, faculties); + await NetworkRouter.loginInSigarra(username, password, faculties); if (isPasswordExpired(responseHtml)) { action.completeError(ExpiredCredentialsException()); } else { @@ -91,9 +89,9 @@ class SessionProvider extends StateProviderNotifier { //notifyListeners(); if (session.authenticated) { - await loadUserProfileInfoFromRemote(stateProviders); + //await loadUserProfileInfoFromRemote(stateProviders); Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); updateStatus(RequestStatus.successful); action?.complete(); } else { diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 265a7ad52..45d344f63 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -16,7 +16,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { notifyListeners(); } - void ensureInitialized(Session session, Profile profile) async { + Future ensureInitialized(Session session, Profile profile) async { if (_initialized) { return; } @@ -30,17 +30,17 @@ abstract class StateProviderNotifier extends ChangeNotifier { final sessionIsPersistent = userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; if (sessionIsPersistent) { - loadFromStorage(); + await loadFromStorage(); } if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - loadFromRemote(session, profile); + await loadFromRemote(session, profile); } notifyListeners(); } - void loadFromStorage(); + Future loadFromStorage(); - void loadFromRemote(Session session, Profile profile); + Future loadFromRemote(Session session, Profile profile); } diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index e815bcd1b..adbff9f5c 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -29,8 +29,7 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { return LazyConsumer(builder: (context, profileProvider) { - final List courseUnits = - profileProvider.profile.currentCourseUnits; + final List courseUnits = profileProvider.profile.courseUnits; List availableYears = []; List availableSemesters = []; if (courseUnits.isNotEmpty) { @@ -56,7 +55,7 @@ class CourseUnitsPageViewState return _getPageView(courseUnits, profileProvider.status, availableYears, availableSemesters); } else { - return Container(); + return _getPageView([], profileProvider.status, [], []); } }); } diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 42b4f10d6..970b30862 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -4,6 +4,12 @@ import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +/// Wrapper around Consumer that ensures that the provider is initialized, +/// meaning that it has loaded its data from storage and/or remote. +/// The provider will not reload its data if it has already been loaded before. +/// The user session should be valid before calling this widget. +/// There must be a SessionProvider and a ProfileProvider above this widget in +/// the widget tree. class LazyConsumer extends StatelessWidget { final Widget Function(BuildContext, T) builder; @@ -14,13 +20,15 @@ class LazyConsumer extends StatelessWidget { @override Widget build(BuildContext context) { - final session = - Provider.of(context, listen: false).session; - final profile = - Provider.of(context, listen: false).profile; + final sessionProvider = Provider.of(context); + final profileProvider = Provider.of(context); + WidgetsBinding.instance.addPostFrameCallback((_) { - Provider.of(context, listen: false) - .ensureInitialized(session, profile); + final session = sessionProvider.session; + final profile = profileProvider.profile; + profileProvider.ensureInitialized(session, profile).then((value) => + Provider.of(context, listen: false) + .ensureInitialized(session, profile)); }); return Consumer(builder: (context, provider, _) { diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 738ee8ba2..9b2b431c3 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -222,8 +222,8 @@ class LoginPageViewState extends State { /// Creates a widget for the user login depending on the status of his login. Widget createStatusWidget(BuildContext context) { - return LazyConsumer( - builder: (context, sessionProvider) { + return Consumer( + builder: (context, sessionProvider, _) { switch (sessionProvider.status) { case RequestStatus.busy: return const SizedBox( diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index ad3538caf..f59f30574 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/session_provider.dart'; -import 'package:uni/view/lazy_consumer.dart'; class ProfileOverview extends StatelessWidget { final Profile profile; @@ -18,8 +18,8 @@ class ProfileOverview extends StatelessWidget { @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, sessionProvider) { + return Consumer( + builder: (context, sessionProvider, _) { return FutureBuilder( future: loadProfilePicture(sessionProvider.session), builder: (BuildContext context, AsyncSnapshot profilePic) => diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index caa578bc4..130b469fd 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -131,7 +131,7 @@ class SplashScreenState extends State { if (mounted) { final List faculties = await AppSharedPreferences.getUserFaculties(); - stateProviders.sessionProvider + await stateProviders.sessionProvider .reLogin(userName, password, faculties, stateProviders); } return MaterialPageRoute(builder: (context) => const HomePageView()); From d8c6730e18da5e1f10e810936cbade9fe20eb75b Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 16:58:34 +0100 Subject: [PATCH 034/100] Simplify RequestDependentWidgetBuilder --- .../fetchers/all_course_units_fetcher.dart | 3 - .../providers/faculty_locations_provider.dart | 8 ++- .../request_dependent_widget_builder.dart | 60 +++++++------------ uni/lib/view/locations/locations.dart | 15 +++-- .../view/restaurant/restaurant_page_view.dart | 1 + 5 files changed, 33 insertions(+), 54 deletions(-) diff --git a/uni/lib/controller/fetchers/all_course_units_fetcher.dart b/uni/lib/controller/fetchers/all_course_units_fetcher.dart index 5d876b1d7..baa454ebf 100644 --- a/uni/lib/controller/fetchers/all_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/all_course_units_fetcher.dart @@ -10,14 +10,11 @@ class AllCourseUnitsFetcher { List courses, Session session) async { final List allCourseUnits = []; for (var course in courses) { - print("course: ${course.name}"); try { final List courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( course, session); - print("courseUnits: ${courseUnits.length}"); allCourseUnits.addAll(courseUnits.where((c) => c.enrollmentIsValid())); - print("allCourseUnits: ${allCourseUnits.length}"); } catch (e) { Logger().e('Failed to fetch course units for ${course.name}', e); } diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart index e02d7905a..6d9468272 100644 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ b/uni/lib/model/providers/faculty_locations_provider.dart @@ -2,10 +2,10 @@ import 'dart:collection'; import 'package:uni/controller/fetchers/location_fetcher/location_fetcher_asset.dart'; 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 '../entities/profile.dart'; -import '../entities/session.dart'; +import 'package:uni/model/request_status.dart'; class FacultyLocationsProvider extends StateProviderNotifier { List _locations = []; @@ -15,7 +15,9 @@ class FacultyLocationsProvider extends StateProviderNotifier { @override Future loadFromStorage() async { + updateStatus(RequestStatus.busy); _locations = await LocationFetcherAsset().getLocations(); + updateStatus(RequestStatus.successful); } @override diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index edf31fa4f..a1c2c363a 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -1,11 +1,8 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/lazy_consumer.dart'; /// Wraps content given its fetch data from the redux store, /// hydrating the component, displaying an empty message, @@ -30,46 +27,29 @@ class RequestDependentWidgetBuilder extends StatelessWidget { final dynamic content; final bool contentChecker; final Widget onNullContent; - static final AppLastUserInfoUpdateDatabase lastUpdateDatabase = - AppLastUserInfoUpdateDatabase(); @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, lastUserInfoProvider) { - switch (status) { - case RequestStatus.successful: - case RequestStatus.none: - return contentChecker - ? contentGenerator(content, context) - : onNullContent; - case RequestStatus.busy: - if (lastUserInfoProvider.lastUpdateTime != null) { - return contentChecker - ? contentGenerator(content, context) - : onNullContent; - } - if (contentLoadingWidget != null) { - return contentChecker - ? contentGenerator(content, context) - : Center( - child: Shimmer.fromColors( - baseColor: Theme.of(context).highlightColor, - highlightColor: - Theme.of(context).colorScheme.onPrimary, - child: contentLoadingWidget!)); - } - return contentChecker - ? contentGenerator(content, context) - : const Center(child: CircularProgressIndicator()); - case RequestStatus.failed: - default: - return contentChecker - ? contentGenerator(content, context) - : requestFailedMessage(); - } - }, - ); + if (status == RequestStatus.busy && !contentChecker) { + return loadingWidget(); + } else if (status == RequestStatus.failed) { + return requestFailedMessage(); + } + + return contentChecker ? contentGenerator(content, context) : onNullContent; + } + + Widget loadingWidget() { + return contentLoadingWidget == null + ? const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: CircularProgressIndicator())) + : Center( + child: Shimmer.fromColors( + baseColor: Theme.of(context).highlightColor, + highlightColor: Theme.of(context).colorScheme.onPrimary, + child: contentLoadingWidget!)); } Widget requestFailedMessage() { diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index 5c7c0e7de..53b7ac05c 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -7,7 +7,6 @@ import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locations/widgets/faculty_maps.dart'; -import 'package:uni/view/locations/widgets/map.dart'; import 'package:uni/view/locations/widgets/marker.dart'; class LocationsPage extends StatefulWidget { @@ -39,11 +38,11 @@ class LocationsPageState extends GeneralPageViewState } class LocationsPageView extends StatelessWidget { - final List? locations; - final RequestStatus? status; + final List locations; + final RequestStatus status; const LocationsPageView( - {super.key, this.locations, this.status = RequestStatus.none}); + {super.key, required this.locations, this.status = RequestStatus.none}); @override Widget build(BuildContext context) { @@ -67,11 +66,11 @@ class LocationsPageView extends StatelessWidget { //TODO:: add support for multiple faculties } - LocationsMap? getMap(BuildContext context) { - if (locations == null || status != RequestStatus.successful) { - return null; + Widget getMap(BuildContext context) { + if (status != RequestStatus.successful) { + return const Center(child: CircularProgressIndicator()); } - return FacultyMaps.getFeupMap(locations!); + return FacultyMaps.getFeupMap(locations); } String getLocation() { diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 852b6b1ab..7b8cb0174 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -115,6 +115,7 @@ class RestaurantDay extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: const [ + SizedBox(height: 10), Center( child: Text("Não há informação disponível sobre refeições")), ], From 0365cf968a702986a13ab7d3626d5c5f786cc060 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 17:45:48 +0100 Subject: [PATCH 035/100] Further simplify RequestDependentWidget --- uni/lib/controller/load_info.dart | 36 ------------------- uni/lib/main.dart | 4 --- .../providers/last_user_info_provider.dart | 30 ---------------- .../providers/state_provider_notifier.dart | 3 ++ uni/lib/model/providers/state_providers.dart | 6 ---- uni/lib/view/calendar/calendar.dart | 22 ++++++------ .../common_widgets/last_update_timestamp.dart | 22 +++++++----- .../request_dependent_widget_builder.dart | 25 +++++++------ uni/lib/view/course_units/course_units.dart | 7 ++-- uni/lib/view/home/widgets/exam_card.dart | 6 ++-- .../view/home/widgets/restaurant_card.dart | 7 ++-- uni/lib/view/home/widgets/schedule_card.dart | 6 ++-- uni/lib/view/lazy_consumer.dart | 28 ++++++++------- .../widgets/library_occupation_card.dart | 7 ++-- .../view/restaurant/restaurant_page_view.dart | 7 ++-- uni/lib/view/schedule/schedule.dart | 25 +++++-------- 16 files changed, 81 insertions(+), 160 deletions(-) delete mode 100644 uni/lib/model/providers/last_user_info_provider.dart diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index 746af7fd6..6432a6776 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -6,44 +6,8 @@ import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_providers.dart'; -/*Future loadReloginInfo(StateProviders stateProviders) async { - final Tuple2 userPersistentCredentials = - await AppSharedPreferences.getPersistentUserInfo(); - final String userName = userPersistentCredentials.item1; - final String password = userPersistentCredentials.item2; - final List faculties = await AppSharedPreferences.getUserFaculties(); - - if (userName != '' && password != '') { - final action = Completer(); - stateProviders.sessionProvider - .reLogin(userName, password, faculties, stateProviders, action: action); - return action.future; - } - return Future.error('No credentials stored'); -}*/ - -Future loadUserProfileInfoFromRemote(StateProviders stateProviders) async { - /*if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { - return; - } - - Logger().i('Loading remote info'); - - final session = stateProviders.sessionProvider.session; - if (!session.authenticated && session.persistentSession) { - await loadReloginInfo(stateProviders); - }*/ - - stateProviders.profileStateProvider - .fetchUserInfo(Completer(), stateProviders.sessionProvider.session); - - stateProviders.lastUserInfoProvider - .setLastUserInfoUpdateTimestamp(Completer()); -} - Future handleRefresh(StateProviders stateProviders) async { Logger().e('TODO: handleRefresh'); - // await loadRemoteUserInfoToState(stateProviders); } Future loadProfilePicture(Session session, {forceRetrieval = false}) { diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 1a61266e0..9628b2398 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -13,7 +13,6 @@ import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; import 'package:uni/model/providers/home_page_provider.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/model/providers/profile_provider.dart'; @@ -56,7 +55,6 @@ Future main() async { CalendarProvider(), LibraryOccupationProvider(), FacultyLocationsProvider(), - LastUserInfoProvider(), HomePageProvider()); OnStartUp.onStart(stateProviders.sessionProvider); @@ -95,8 +93,6 @@ Future main() async { ChangeNotifierProvider( create: (context) => stateProviders.facultyLocationsProvider), - ChangeNotifierProvider( - create: (context) => stateProviders.lastUserInfoProvider), ChangeNotifierProvider( create: (context) => stateProviders.homePageProvider), ], diff --git a/uni/lib/model/providers/last_user_info_provider.dart b/uni/lib/model/providers/last_user_info_provider.dart deleted file mode 100644 index fb9468c89..000000000 --- a/uni/lib/model/providers/last_user_info_provider.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:async'; - -import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; -import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/state_provider_notifier.dart'; - -class LastUserInfoProvider extends StateProviderNotifier { - DateTime? _lastUpdateTime; - - DateTime? get lastUpdateTime => _lastUpdateTime; - - setLastUserInfoUpdateTimestamp(Completer action) async { - _lastUpdateTime = DateTime.now(); - notifyListeners(); - final AppLastUserInfoUpdateDatabase db = AppLastUserInfoUpdateDatabase(); - await db.insertNewTimeStamp(_lastUpdateTime!); - action.complete(); - } - - @override - Future loadFromStorage() async { - final AppLastUserInfoUpdateDatabase db = AppLastUserInfoUpdateDatabase(); - _lastUpdateTime = await db.getLastUserInfoUpdateTime(); - notifyListeners(); - } - - @override - Future loadFromRemote(Session session, Profile profile) async {} -} diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 45d344f63..58cbcfe1a 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -8,9 +8,12 @@ import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { RequestStatus _status = RequestStatus.none; bool _initialized = false; + DateTime? _lastUpdateTime; RequestStatus get status => _status; + DateTime? get lastUpdateTime => _lastUpdateTime; + void updateStatus(RequestStatus status) { _status = status; notifyListeners(); diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index af0287b06..4262f541e 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -5,7 +5,6 @@ import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/model/providers/exam_provider.dart'; import 'package:uni/model/providers/faculty_locations_provider.dart'; import 'package:uni/model/providers/home_page_provider.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/model/providers/profile_provider.dart'; @@ -22,7 +21,6 @@ class StateProviders { final CalendarProvider calendarProvider; final LibraryOccupationProvider libraryOccupationProvider; final FacultyLocationsProvider facultyLocationsProvider; - final LastUserInfoProvider lastUserInfoProvider; final HomePageProvider homePageProvider; StateProviders( @@ -35,7 +33,6 @@ class StateProviders { this.calendarProvider, this.libraryOccupationProvider, this.facultyLocationsProvider, - this.lastUserInfoProvider, this.homePageProvider); static StateProviders fromContext(BuildContext context) { @@ -56,8 +53,6 @@ class StateProviders { Provider.of(context, listen: false); final facultyLocationsProvider = Provider.of(context, listen: false); - final lastUserInfoProvider = - Provider.of(context, listen: false); final homePageProvider = Provider.of(context, listen: false); @@ -71,7 +66,6 @@ class StateProviders { calendarProvider, libraryOccupationProvider, facultyLocationsProvider, - lastUserInfoProvider, homePageProvider); } } diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 181ec4036..8a80a3928 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -4,6 +4,7 @@ import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/providers/calendar_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/lazy_consumer.dart'; class CalendarPageView extends StatefulWidget { @@ -17,18 +18,19 @@ class CalendarPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, calendarProvider) => - getCalendarPage(context, calendarProvider.calendar), - ); - } - - Widget getCalendarPage(BuildContext context, List calendar) { - return ListView( - children: [_getPageTitle(), getTimeline(context, calendar)]); + builder: (context, calendarProvider) => ListView(children: [ + _getPageTitle(), + RequestDependentWidgetBuilder( + status: calendarProvider.status, + builder: () => + getTimeline(context, calendarProvider.calendar), + hasContentPredicate: calendarProvider.calendar.isNotEmpty, + onNullContent: const Center( + child: Text('Nenhum evento encontrado', + style: TextStyle(fontSize: 18.0)))) + ])); } - // TODO - Widget _getPageTitle() { return Container( padding: const EdgeInsets.only(bottom: 6.0), diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index a996352c1..b674d1f05 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -1,19 +1,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/view/lazy_consumer.dart'; -class LastUpdateTimeStamp extends StatefulWidget { +class LastUpdateTimeStamp + extends StatefulWidget { const LastUpdateTimeStamp({super.key}); @override State createState() { - return _LastUpdateTimeStampState(); + return _LastUpdateTimeStampState(); } } -class _LastUpdateTimeStampState extends State { +class _LastUpdateTimeStampState + extends State { DateTime currentTime = DateTime.now(); @override @@ -33,11 +35,13 @@ class _LastUpdateTimeStampState extends State { @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, lastUserInfoProvider) => Container( - padding: const EdgeInsets.only(top: 8.0, bottom: 10.0), - child: _getContent(context, lastUserInfoProvider.lastUpdateTime!)), - ); + return LazyConsumer( + builder: (context, provider) => Container( + padding: const EdgeInsets.only(top: 8.0, bottom: 10.0), + child: provider.lastUpdateTime != null + ? _getContent(context, provider.lastUpdateTime!) + : null, + )); } Widget _getContent(BuildContext context, DateTime lastUpdateTime) { diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index a1c2c363a..29502ddf1 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -7,39 +7,38 @@ import 'package:uni/utils/drawer_items.dart'; /// Wraps content given its fetch data from the redux store, /// hydrating the component, displaying an empty message, /// a connection error or a loading circular effect as appropriate - class RequestDependentWidgetBuilder extends StatelessWidget { const RequestDependentWidgetBuilder( {Key? key, - required this.context, required this.status, - required this.contentGenerator, - required this.content, - required this.contentChecker, + required this.builder, + required this.hasContentPredicate, required this.onNullContent, this.contentLoadingWidget}) : super(key: key); - final BuildContext context; final RequestStatus status; - final Widget Function(dynamic, BuildContext) contentGenerator; + final Widget Function() builder; final Widget? contentLoadingWidget; - final dynamic content; - final bool contentChecker; + final bool hasContentPredicate; final Widget onNullContent; @override Widget build(BuildContext context) { - if (status == RequestStatus.busy && !contentChecker) { - return loadingWidget(); + if (status == RequestStatus.busy && !hasContentPredicate) { + return loadingWidget(context); } else if (status == RequestStatus.failed) { return requestFailedMessage(); } - return contentChecker ? contentGenerator(content, context) : onNullContent; + return hasContentPredicate + ? builder() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: onNullContent); } - Widget loadingWidget() { + Widget loadingWidget(BuildContext context) { return contentLoadingWidget == null ? const Center( child: Padding( diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index adbff9f5c..e6ec3ac79 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -78,11 +78,10 @@ class CourseUnitsPageViewState return Column(children: [ _getPageTitleAndFilters(availableYears, availableSemesters), RequestDependentWidgetBuilder( - context: context, status: requestStatus ?? RequestStatus.none, - contentGenerator: _generateCourseUnitsCards, - content: filteredCourseUnits ?? [], - contentChecker: courseUnits?.isNotEmpty ?? false, + builder: () => + _generateCourseUnitsCards(filteredCourseUnits, context), + hasContentPredicate: courseUnits?.isNotEmpty ?? false, onNullContent: Center( heightFactor: 10, child: Text('Não existem cadeiras para apresentar', diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index d00e962d0..9b2285100 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -39,11 +39,9 @@ class ExamCard extends GenericCard { .where((exam) => (!hiddenExams.contains(exam.id))) .toList(); return RequestDependentWidgetBuilder( - context: context, status: examProvider.status, - contentGenerator: generateExams, - content: exams, - contentChecker: exams.isNotEmpty, + builder: () => generateExams(exams, context), + hasContentPredicate: exams.isNotEmpty, onNullContent: Center( child: Text('Não existem exames para apresentar', style: Theme.of(context).textTheme.titleLarge), diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 0f7c248dc..34c892dcc 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -24,11 +24,10 @@ class RestaurantCard extends GenericCard { Widget buildCardContent(BuildContext context) { return LazyConsumer( builder: (context, restaurantProvider) => RequestDependentWidgetBuilder( - context: context, status: restaurantProvider.status, - contentGenerator: generateRestaurant, - content: restaurantProvider.restaurants, - contentChecker: restaurantProvider.restaurants.isNotEmpty, + builder: () => + generateRestaurant(restaurantProvider.restaurants, context), + hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, onNullContent: Center( child: Text('Não existem cantinas para apresentar', style: Theme.of(context).textTheme.headlineMedium, diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index cf4ff2214..414d1c9e9 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -25,11 +25,9 @@ class ScheduleCard extends GenericCard { Widget buildCardContent(BuildContext context) { return LazyConsumer( builder: (context, lectureProvider) => RequestDependentWidgetBuilder( - context: context, status: lectureProvider.status, - contentGenerator: generateSchedule, - content: lectureProvider.lectures, - contentChecker: lectureProvider.lectures.isNotEmpty, + builder: () => generateSchedule(lectureProvider.lectures, context), + hasContentPredicate: lectureProvider.lectures.isNotEmpty, onNullContent: Center( child: Text('Não existem aulas para apresentar', style: Theme.of(context).textTheme.titleLarge, diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 970b30862..bcde3ae1f 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -7,9 +7,8 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; /// Wrapper around Consumer that ensures that the provider is initialized, /// meaning that it has loaded its data from storage and/or remote. /// The provider will not reload its data if it has already been loaded before. -/// The user session should be valid before calling this widget. -/// There must be a SessionProvider and a ProfileProvider above this widget in -/// the widget tree. +/// There should be a SessionProvider and a ProfileProvider above this widget in +/// the widget tree to initialize the provider data the first time. class LazyConsumer extends StatelessWidget { final Widget Function(BuildContext, T) builder; @@ -20,16 +19,21 @@ class LazyConsumer extends StatelessWidget { @override Widget build(BuildContext context) { - final sessionProvider = Provider.of(context); - final profileProvider = Provider.of(context); + try { + final sessionProvider = Provider.of(context); + final profileProvider = Provider.of(context); - WidgetsBinding.instance.addPostFrameCallback((_) { - final session = sessionProvider.session; - final profile = profileProvider.profile; - profileProvider.ensureInitialized(session, profile).then((value) => - Provider.of(context, listen: false) - .ensureInitialized(session, profile)); - }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final session = sessionProvider.session; + final profile = profileProvider.profile; + profileProvider.ensureInitialized(session, profile).then((value) => + Provider.of(context, listen: false) + .ensureInitialized(session, profile)); + }); + } catch (_) { + // The provider won't be initialized + // Should only happen in tests + } return Consumer(builder: (context, provider, _) { return builder(context, provider); diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index be02eee33..d4e9c6027 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -27,11 +27,10 @@ class LibraryOccupationCard extends GenericCard { return LazyConsumer( builder: (context, libraryOccupationProvider) => RequestDependentWidgetBuilder( - context: context, status: libraryOccupationProvider.status, - contentGenerator: generateOccupation, - content: libraryOccupationProvider.occupation, - contentChecker: + builder: () => generateOccupation( + libraryOccupationProvider.occupation, context), + hasContentPredicate: libraryOccupationProvider.status != RequestStatus.busy, onNullContent: const CircularProgressIndicator())); } diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 7b8cb0174..3be1417b2 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -52,11 +52,10 @@ class _RestaurantPageState extends GeneralPageViewState ]), const SizedBox(height: 10), RequestDependentWidgetBuilder( - context: context, status: restaurantProvider.status, - contentGenerator: createTabViewBuilder, - content: restaurantProvider.restaurants, - contentChecker: restaurantProvider.restaurants.isNotEmpty, + builder: () => + createTabViewBuilder(restaurantProvider.restaurants, context), + hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, onNullContent: const Center(child: Text('Não há refeições disponíveis.'))) ]); diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 7943f89d3..ff5dd4f69 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -154,29 +154,22 @@ class SchedulePageViewState extends GeneralPageViewState return scheduleContent; } - Widget Function(dynamic daycontent, BuildContext context) dayColumnBuilder( - int day) { - Widget createDayColumn(dayContent, BuildContext context) { - return Container( - key: Key('schedule-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: createScheduleRows(dayContent, context), - )); - } - - return createDayColumn; + Widget dayColumnBuilder(int day, dayContent, BuildContext context) { + return Container( + key: Key('schedule-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: createScheduleRows(dayContent, context), + )); } Widget createScheduleByDay(BuildContext context, int day, List? lectures, RequestStatus? scheduleStatus) { final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); return RequestDependentWidgetBuilder( - context: context, status: scheduleStatus ?? RequestStatus.none, - contentGenerator: dayColumnBuilder(day), - content: aggLectures[day], - contentChecker: aggLectures[day].isNotEmpty, + builder: () => dayColumnBuilder(day, aggLectures[day], context), + hasContentPredicate: aggLectures[day].isNotEmpty, onNullContent: Center( child: Text( 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.')), From a3f4b4e3829d6afd2822d2aa58be72bcd36dae5c Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 17:49:04 +0100 Subject: [PATCH 036/100] Fix tests --- .../integration/src/schedule_page_test.dart | 2 -- .../view/Pages/schedule_page_view_test.dart | 23 ++++++------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index ba74d834a..d4ffa5e8c 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -14,7 +14,6 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/view/schedule/schedule.dart'; @@ -54,7 +53,6 @@ void main() { final providers = [ ChangeNotifierProvider(create: (_) => scheduleProvider), - ChangeNotifierProvider(create: (_) => LastUserInfoProvider()), ]; await tester.pumpWidget(testableWidget(widget, providers: providers)); diff --git a/uni/test/unit/view/Pages/schedule_page_view_test.dart b/uni/test/unit/view/Pages/schedule_page_view_test.dart index 3faa375ef..e8eea58d1 100644 --- a/uni/test/unit/view/Pages/schedule_page_view_test.dart +++ b/uni/test/unit/view/Pages/schedule_page_view_test.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; -import 'package:uni/model/providers/last_user_info_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/view/schedule/schedule.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; @@ -21,10 +19,10 @@ void main() { final lecture1 = Lecture.fromHtml( 'SOPE', 'T', day0, '10:00', blocks, 'B315', 'JAS', classNumber, 484378); - final lecture2 = Lecture.fromHtml( - 'SDIS', 'T', day0, '13:00', blocks, 'B315', 'PMMS', classNumber, 484381); - final lecture3 = Lecture.fromHtml( - 'AMAT', 'T', day1, '12:00', blocks, 'B315', 'PMMS', classNumber, 484362); + final lecture2 = Lecture.fromHtml('SDIS', 'T', day0, '13:00', blocks, + 'B315', 'PMMS', classNumber, 484381); + final lecture3 = Lecture.fromHtml('AMAT', 'T', day1, '12:00', blocks, + 'B315', 'PMMS', classNumber, 484362); final lecture4 = Lecture.fromHtml( 'PROG', 'T', day2, '10:00', blocks, 'B315', 'JAS', classNumber, 484422); final lecture5 = Lecture.fromHtml( @@ -40,17 +38,12 @@ void main() { 'Sexta-feira' ]; - final providers = [ - ChangeNotifierProvider( - create: (_) => LastUserInfoProvider()), - ]; - testWidgets('When given one lecture on a single day', (WidgetTester tester) async { final widget = SchedulePageView( lectures: [lecture1], scheduleStatus: RequestStatus.successful); - await tester.pumpWidget(testableWidget(widget, providers: providers)); + await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); final SchedulePageViewState myWidgetState = tester.state(find.byType(SchedulePageView)); @@ -69,7 +62,7 @@ void main() { final widget = SchedulePageView( lectures: [lecture1, lecture2], scheduleStatus: RequestStatus.successful); - await tester.pumpWidget(testableWidget(widget, providers: providers)); + await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); final SchedulePageViewState myWidgetState = tester.state(find.byType(SchedulePageView)); @@ -95,9 +88,7 @@ void main() { lecture6 ], scheduleStatus: RequestStatus.successful)); - - - await tester.pumpWidget(testableWidget(widget, providers: providers)); + await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); final SchedulePageViewState myWidgetState = tester.state(find.byType(SchedulePageView)); From 4e14bdf3e513a4f49ddd735a27b525626aa77529 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 18:08:56 +0100 Subject: [PATCH 037/100] Fix last update timestamp widget --- .../local_storage/app_shared_preferences.dart | 23 +++++++++++++++---- .../providers/state_provider_notifier.dart | 6 +++++ .../bus_stop_next_arrivals.dart | 2 +- .../widgets/bus_stop_row.dart | 11 +++++---- .../bus_stop_selection.dart | 2 +- uni/lib/view/home/widgets/bus_stop_card.dart | 2 +- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/app_shared_preferences.dart index 9134e9ef4..6db64b9ed 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/app_shared_preferences.dart @@ -12,12 +12,14 @@ import 'package:uni/utils/favorite_widget_type.dart'; /// This database stores the user's student number, password and favorite /// widgets. class AppSharedPreferences { + static const lastUpdateTimeKeySuffix = "_last_update_time"; static const String userNumber = 'user_number'; static const String userPw = 'user_password'; static const String userFaculties = 'user_faculties'; static const String termsAndConditions = 'terms_and_conditions'; static const String areTermsAndConditionsAcceptedKey = 'is_t&c_accepted'; - static const String tuitionNotificationsToggleKey = "tuition_notification_toogle"; + static const String tuitionNotificationsToggleKey = + "tuition_notification_toogle"; static const String themeMode = 'theme_mode'; static const int keyLength = 32; static const int ivLength = 16; @@ -33,6 +35,20 @@ class AppSharedPreferences { static const String filteredExamsTypes = 'filtered_exam_types'; static final List defaultFilteredExamTypes = Exam.displayedTypes; + /// Returns the last time the data with given key was updated. + static Future getLastDataClassUpdateTime(String dataKey) async { + final prefs = await SharedPreferences.getInstance(); + final lastUpdateTime = prefs.getString(dataKey + lastUpdateTimeKeySuffix); + return lastUpdateTime != null ? DateTime.parse(lastUpdateTime) : null; + } + + /// Sets the last time the data with given key was updated. + static Future setLastDataClassUpdateTime( + String dataKey, DateTime dateTime) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(dataKey + lastUpdateTimeKeySuffix, dateTime.toString()); + } + /// Saves the user's student number, password and faculties. static Future savePersistentUserInfo(user, pass, faculties) async { final prefs = await SharedPreferences.getInstance(); @@ -203,14 +219,13 @@ class AppSharedPreferences { return encrypt.Encrypter(encrypt.AES(key)); } - static Future getTuitionNotificationToggle() async{ + static Future getTuitionNotificationToggle() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(tuitionNotificationsToggleKey) ?? true; } - static setTuitionNotificationToggle(bool value) async{ + static setTuitionNotificationToggle(bool value) async { final prefs = await SharedPreferences.getInstance(); prefs.setBool(tuitionNotificationsToggleKey, value); } - } diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 58cbcfe1a..b653cb719 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -26,6 +26,9 @@ abstract class StateProviderNotifier extends ChangeNotifier { _initialized = true; + _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( + runtimeType.toString()); + updateStatus(RequestStatus.busy); final userPersistentInfo = @@ -38,6 +41,9 @@ abstract class StateProviderNotifier extends ChangeNotifier { if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { await loadFromRemote(session, profile); + _lastUpdateTime = DateTime.now(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); } notifyListeners(); diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index ad2c15092..f730eff7c 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -126,7 +126,7 @@ class NextArrivalsState extends State { children: [ Container( padding: const EdgeInsets.only(left: 10.0), - child: const LastUpdateTimeStamp(), + child: const LastUpdateTimeStamp(), ), IconButton( icon: const Icon(Icons.edit), diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart index f0712c2c9..ce09b350b 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart @@ -46,10 +46,13 @@ class BusStopRow extends StatelessWidget { } Widget noTripsContainer(context) { - return Text('Não há viagens planeadas de momento.', - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Text('Não há viagens planeadas de momento.', + maxLines: 3, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium)); } Widget stopCodeRotatedContainer(context) { diff --git a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart index 08eb2dd8a..c4ee72861 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -49,7 +49,7 @@ class BusStopSelectionPageState Container( padding: const EdgeInsets.all(20.0), child: const Text( - '''Os autocarros favoritos serão apresentados no widget 'Autocarros' dos favoritos.''' + '''Os autocarros favoritos serão apresentados no widget 'Autocarros' dos favoritos. ''' '''Os restantes serão apresentados apenas na página.''', textAlign: TextAlign.center)), Column(children: rows), diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index d5a12acb1..7097b8e7f 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -115,7 +115,7 @@ Widget getBusStopsInfo(context, Map stopData) { List getEachBusStopInfo(context, Map stopData) { final List rows = []; - rows.add(const LastUpdateTimeStamp()); + rows.add(const LastUpdateTimeStamp()); stopData.forEach((stopCode, stopInfo) { if (stopInfo.trips.isNotEmpty && stopInfo.favorited) { From 6aee5521c0a3b9bc8e37fe5b6a387128d50bfda4 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 18:50:26 +0100 Subject: [PATCH 038/100] Make each page responsible for refreshing --- uni/lib/controller/load_info.dart | 6 ---- .../providers/state_provider_notifier.dart | 29 +++++++++++++++---- uni/lib/view/about/about.dart | 5 +++- uni/lib/view/bug_report/bug_report.dart | 3 ++ .../bus_stop_next_arrivals.dart | 7 +++++ .../bus_stop_selection.dart | 3 ++ uni/lib/view/calendar/calendar.dart | 7 +++++ .../pages_layouts/general/general.dart | 18 ++++-------- .../course_unit_info/course_unit_info.dart | 5 +++- uni/lib/view/course_units/course_units.dart | 7 +++++ uni/lib/view/exams/exams.dart | 6 ++++ uni/lib/view/home/home.dart | 6 ++++ uni/lib/view/library/library.dart | 7 +++++ uni/lib/view/locations/locations.dart | 5 +++- uni/lib/view/login/login.dart | 2 -- uni/lib/view/profile/profile.dart | 7 +++++ .../view/restaurant/restaurant_page_view.dart | 7 +++++ uni/lib/view/schedule/schedule.dart | 7 +++++ uni/lib/view/useful_info/useful_info.dart | 5 +++- 19 files changed, 112 insertions(+), 30 deletions(-) diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart index 6432a6776..92fe751c4 100644 --- a/uni/lib/controller/load_info.dart +++ b/uni/lib/controller/load_info.dart @@ -1,14 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:logger/logger.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/state_providers.dart'; - -Future handleRefresh(StateProviders stateProviders) async { - Logger().e('TODO: handleRefresh'); -} Future loadProfilePicture(Session session, {forceRetrieval = false}) { final String studentNumber = session.studentNumber; diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index b653cb719..54caedfd2 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,8 +1,11 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; 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/profile_provider.dart'; +import 'package:uni/model/providers/session_provider.dart'; import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { @@ -14,11 +17,30 @@ abstract class StateProviderNotifier extends ChangeNotifier { DateTime? get lastUpdateTime => _lastUpdateTime; + Future _loadFromRemote(Session session, Profile profile) async { + if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { + await loadFromRemote(session, profile); + _lastUpdateTime = DateTime.now(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); + } + } + void updateStatus(RequestStatus status) { _status = status; notifyListeners(); } + Future forceRefresh(BuildContext context) async { + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; + + updateStatus(RequestStatus.busy); + _loadFromRemote(session, profile); + } + Future ensureInitialized(Session session, Profile profile) async { if (_initialized) { return; @@ -39,12 +61,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { await loadFromStorage(); } - if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - await loadFromRemote(session, profile); - _lastUpdateTime = DateTime.now(); - await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); - } + _loadFromRemote(session, profile); notifyListeners(); } diff --git a/uni/lib/view/about/about.dart b/uni/lib/view/about/about.dart index 411a1901d..f1a66c3e0 100644 --- a/uni/lib/view/about/about.dart +++ b/uni/lib/view/about/about.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/about/widgets/terms_and_conditions.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; class AboutPageView extends StatefulWidget { const AboutPageView({super.key}); @@ -38,4 +38,7 @@ class AboutPageViewState extends GeneralPageViewState { ], ); } + + @override + Future handleRefresh(BuildContext context) async {} } diff --git a/uni/lib/view/bug_report/bug_report.dart b/uni/lib/view/bug_report/bug_report.dart index 6a263d31a..32db38934 100644 --- a/uni/lib/view/bug_report/bug_report.dart +++ b/uni/lib/view/bug_report/bug_report.dart @@ -18,4 +18,7 @@ class BugReportPageViewState extends GeneralPageViewState { margin: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0), child: const BugReportForm()); } + + @override + Future handleRefresh(BuildContext context) async {} } diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index f730eff7c..60677bc01 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; @@ -26,6 +27,12 @@ class BusStopNextArrivalsPageState NextArrivals(busProvider.configuredBusStops, busProvider.status) ])); } + + @override + Future handleRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } class NextArrivals extends StatefulWidget { diff --git a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart index c4ee72861..ad4e1e5ad 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -72,4 +72,7 @@ class BusStopSelectionPageState ]); }); } + + @override + Future handleRefresh(BuildContext context) async {} } diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 8a80a3928..6b341be61 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:timelines/timelines.dart'; import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/providers/calendar_provider.dart'; @@ -68,4 +69,10 @@ class CalendarPageViewState extends GeneralPageViewState { ), ); } + + @override + Future handleRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } 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 ad8f9f20e..fde15635f 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -6,7 +6,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/load_info.dart'; import 'package:uni/model/providers/session_provider.dart'; -import 'package:uni/model/providers/state_providers.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart'; import 'package:uni/view/profile/profile.dart'; @@ -16,6 +15,8 @@ abstract class GeneralPageViewState extends State { final double borderMargin = 18.0; static ImageProvider? profileImageProvider; + Future handleRefresh(BuildContext context); + @override Widget build(BuildContext context) { return getScaffold(context, getBody(context)); @@ -52,21 +53,14 @@ abstract class GeneralPageViewState extends State { Widget refreshState(BuildContext context, Widget child) { return RefreshIndicator( key: GlobalKey(), - onRefresh: refreshCallback(context), + onRefresh: () => loadProfilePicture( + Provider.of(context, listen: false).session, + forceRetrieval: true) + .then((value) => handleRefresh(context)), child: child, ); } - Future Function() refreshCallback(BuildContext context) { - return () async { - final stateProviders = StateProviders.fromContext(context); - await loadProfilePicture( - Provider.of(context, listen: false).session, - forceRetrieval: true); - return handleRefresh(stateProviders); - }; - } - Widget getScaffold(BuildContext context, Widget body) { return Scaffold( appBar: buildAppBar(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 b9a28c8cb..37c11f505 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/course_unit.dart'; -import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; class CourseUnitDetailPageView extends StatefulWidget { final CourseUnit courseUnit; @@ -36,4 +36,7 @@ class CourseUnitDetailPageViewState ])) ]); } + + @override + Future handleRefresh(BuildContext context) async {} } diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index e6ec3ac79..d9be172e3 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/course_unit.dart'; import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/model/request_status.dart'; @@ -191,4 +192,10 @@ class CourseUnitsPageViewState .sorted() + [CourseUnitsPageView.bothSemestersDropdownOption]; } + + @override + Future handleRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 5988080be..9413971dc 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -117,4 +117,10 @@ class ExamsPageViewState extends GeneralPageViewState { : Theme.of(context).scaffoldBackgroundColor, child: ExamRow(exam: exam, teacher: '', mainPage: false))); } + + @override + Future handleRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index 613e0c18a..f10d62063 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; @@ -15,4 +16,9 @@ class HomePageViewState extends GeneralPageViewState { Widget getBody(BuildContext context) { return MainCardsList(); } + + @override + Future handleRefresh(BuildContext context) async { + Logger().e('TODO: Iterate over cards and refresh them.'); + } } diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index eb2f3bfc4..9025266e8 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/library_occupation.dart'; import 'package:uni/model/providers/library_occupation_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; @@ -21,6 +22,12 @@ class LibraryPageViewState extends GeneralPageViewState { builder: (context, libraryOccupationProvider) => LibraryPage(libraryOccupationProvider.occupation)); } + + @override + Future handleRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } class LibraryPage extends StatelessWidget { diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index 53b7ac05c..956c636be 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -35,6 +35,9 @@ class LocationsPageState extends GeneralPageViewState }, ); } + + @override + Future handleRefresh(BuildContext context) async {} } class LocationsPageView extends StatelessWidget { @@ -78,7 +81,7 @@ class LocationsPageView extends StatelessWidget { } List getMarkers() { - return locations!.map((location) { + return locations.map((location) { return LocationMarker(location.latlng, location); }).toList(); } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 9b2b431c3..f0395c19f 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -13,8 +13,6 @@ import 'package:uni/view/login/widgets/inputs.dart'; import 'package:uni/view/theme.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../lazy_consumer.dart'; - class LoginPageView extends StatefulWidget { const LoginPageView({super.key}); diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 656477735..97bff9f00 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/providers/profile_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -47,4 +48,10 @@ class ProfilePageViewState extends SecondaryPageViewState { Widget getTopRightButton(BuildContext context) { return Container(); } + + @override + Future handleRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 3be1417b2..b568a82a6 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/providers/restaurant_provider.dart'; @@ -95,6 +96,12 @@ class _RestaurantPageState extends GeneralPageViewState return tabs; } + + @override + Future handleRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } class RestaurantDay extends StatelessWidget { diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index ff5dd4f69..0831d96d0 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lecture_provider.dart'; @@ -175,4 +176,10 @@ class SchedulePageViewState extends GeneralPageViewState 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.')), ); } + + @override + Future handleRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } diff --git a/uni/lib/view/useful_info/useful_info.dart b/uni/lib/view/useful_info/useful_info.dart index cb6b83dc7..e923a6194 100644 --- a/uni/lib/view/useful_info/useful_info.dart +++ b/uni/lib/view/useful_info/useful_info.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/useful_info/widgets/academic_services_card.dart'; import 'package:uni/view/useful_info/widgets/copy_center_card.dart'; @@ -7,7 +8,6 @@ import 'package:uni/view/useful_info/widgets/infodesk_card.dart'; import 'package:uni/view/useful_info/widgets/multimedia_center_card.dart'; import 'package:uni/view/useful_info/widgets/other_links_card.dart'; import 'package:uni/view/useful_info/widgets/sigarra_links_card.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; class UsefulInfoPageView extends StatefulWidget { const UsefulInfoPageView({super.key}); @@ -37,4 +37,7 @@ class UsefulInfoPageViewState extends GeneralPageViewState { padding: const EdgeInsets.only(bottom: 6.0), child: const PageTitle(name: 'Úteis')); } + + @override + Future handleRefresh(BuildContext context) async {} } From b23fe1ba70a7a9d2c3eb2ea968030120c100358e Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 7 Jul 2023 19:21:59 +0100 Subject: [PATCH 039/100] Refresh home page --- .../providers/state_provider_notifier.dart | 3 ++ uni/lib/view/home/home.dart | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 54caedfd2..54afd3938 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -59,6 +59,9 @@ abstract class StateProviderNotifier extends ChangeNotifier { userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; if (sessionIsPersistent) { await loadFromStorage(); + if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { + updateStatus(RequestStatus.none); + } } _loadFromRemote(session, profile); diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index f10d62063..b2f173e12 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,8 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:logger/logger.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/model/providers/library_occupation_provider.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; +import '../../model/providers/home_page_provider.dart'; +import '../../model/providers/profile_provider.dart'; + class HomePageView extends StatefulWidget { const HomePageView({super.key}); @@ -19,6 +28,40 @@ class HomePageViewState extends GeneralPageViewState { @override Future handleRefresh(BuildContext context) async { - Logger().e('TODO: Iterate over cards and refresh them.'); + final homePageProvider = + Provider.of(context, listen: false); + + final providersToUpdate = {}; + + for (final cardType in homePageProvider.favoriteCards) { + switch (cardType) { + case FavoriteWidgetType.account: + providersToUpdate + .add(Provider.of(context, listen: false)); + break; + case FavoriteWidgetType.exams: + providersToUpdate + .add(Provider.of(context, listen: false)); + break; + case FavoriteWidgetType.schedule: + providersToUpdate + .add(Provider.of(context, listen: false)); + break; + case FavoriteWidgetType.printBalance: + providersToUpdate + .add(Provider.of(context, listen: false)); + break; + case FavoriteWidgetType.libraryOccupation: + providersToUpdate.add( + Provider.of(context, listen: false)); + break; + case FavoriteWidgetType.busStops: + providersToUpdate + .add(Provider.of(context, listen: false)); + break; + } + } + + Future.wait(providersToUpdate.map((e) => e.forceRefresh(context))); } } From 068a02a0a005d4ffdfcc912c3f5449ea6311cef3 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 10 Jul 2023 14:40:12 +0100 Subject: [PATCH 040/100] Do not wait for session provider if not dependant on it --- uni/lib/controller/on_start_up.dart | 2 +- uni/lib/main.dart | 20 +++--- .../{ => lazy}/bus_stop_provider.dart | 2 + .../{ => lazy}/calendar_provider.dart | 2 + .../providers/{ => lazy}/exam_provider.dart | 2 + .../faculty_locations_provider.dart | 2 + .../{ => lazy}/home_page_provider.dart | 5 +- .../{ => lazy}/lecture_provider.dart | 2 + .../library_occupation_provider.dart | 2 + .../{ => lazy}/restaurant_provider.dart | 2 + .../{ => startup}/profile_provider.dart | 2 + .../{ => startup}/session_provider.dart | 34 +++------- .../providers/state_provider_notifier.dart | 67 ++++++++++++------- uni/lib/model/providers/state_providers.dart | 20 +++--- .../bus_stop_next_arrivals.dart | 2 +- .../widgets/estimated_arrival_timestamp.dart | 2 +- .../bus_stop_selection.dart | 2 +- .../widgets/bus_stop_search.dart | 2 +- .../widgets/bus_stop_selection_row.dart | 2 +- .../view/bus_stop_selection/widgets/form.dart | 2 +- uni/lib/view/calendar/calendar.dart | 2 +- .../pages_layouts/general/general.dart | 2 +- .../general/widgets/navigation_drawer.dart | 2 +- uni/lib/view/course_units/course_units.dart | 2 +- uni/lib/view/exams/exams.dart | 2 +- .../view/exams/widgets/exam_filter_form.dart | 2 +- .../view/exams/widgets/exam_filter_menu.dart | 2 +- uni/lib/view/exams/widgets/exam_row.dart | 8 +-- uni/lib/view/home/home.dart | 13 ++-- uni/lib/view/home/widgets/bus_stop_card.dart | 2 +- uni/lib/view/home/widgets/exam_card.dart | 2 +- .../view/home/widgets/main_cards_list.dart | 4 +- .../view/home/widgets/restaurant_card.dart | 2 +- uni/lib/view/home/widgets/schedule_card.dart | 2 +- uni/lib/view/lazy_consumer.dart | 23 ++++--- uni/lib/view/library/library.dart | 2 +- .../widgets/library_occupation_card.dart | 2 +- uni/lib/view/locations/locations.dart | 2 +- uni/lib/view/login/login.dart | 5 +- uni/lib/view/profile/profile.dart | 2 +- .../profile/widgets/account_info_card.dart | 2 +- .../widgets/create_print_mb_dialog.dart | 2 +- .../view/profile/widgets/print_info_card.dart | 2 +- .../profile/widgets/profile_overview.dart | 2 +- .../view/restaurant/restaurant_page_view.dart | 2 +- uni/lib/view/schedule/schedule.dart | 2 +- uni/lib/view/splash/splash.dart | 2 +- uni/test/integration/src/exams_page_test.dart | 2 +- .../integration/src/schedule_page_test.dart | 2 +- .../unit/providers/exams_provider_test.dart | 2 +- .../unit/providers/lecture_provider_test.dart | 2 +- .../unit/view/Pages/exams_page_view_test.dart | 64 +++++++----------- uni/test/unit/view/Widgets/exam_row_test.dart | 4 +- 53 files changed, 182 insertions(+), 167 deletions(-) rename uni/lib/model/providers/{ => lazy}/bus_stop_provider.dart (98%) rename uni/lib/model/providers/{ => lazy}/calendar_provider.dart (96%) rename uni/lib/model/providers/{ => lazy}/exam_provider.dart (98%) rename uni/lib/model/providers/{ => lazy}/faculty_locations_provider.dart (93%) rename uni/lib/model/providers/{ => lazy}/home_page_provider.dart (94%) rename uni/lib/model/providers/{ => lazy}/lecture_provider.dart (98%) rename uni/lib/model/providers/{ => lazy}/library_occupation_provider.dart (96%) rename uni/lib/model/providers/{ => lazy}/restaurant_provider.dart (96%) rename uni/lib/model/providers/{ => startup}/profile_provider.dart (99%) rename uni/lib/model/providers/{ => startup}/session_provider.dart (74%) diff --git a/uni/lib/controller/on_start_up.dart b/uni/lib/controller/on_start_up.dart index 48eb205a5..f51e5a032 100644 --- a/uni/lib/controller/on_start_up.dart +++ b/uni/lib/controller/on_start_up.dart @@ -1,5 +1,5 @@ import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/navigation_service.dart'; class OnStartUp { diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 9628b2398..3b5039fe7 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -8,16 +8,16 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/background_workers/background_callback.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/on_start_up.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; -import 'package:uni/model/providers/calendar_provider.dart'; -import 'package:uni/model/providers/exam_provider.dart'; -import 'package:uni/model/providers/faculty_locations_provider.dart'; -import 'package:uni/model/providers/home_page_provider.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; -import 'package:uni/model/providers/library_occupation_provider.dart'; -import 'package:uni/model/providers/profile_provider.dart'; -import 'package:uni/model/providers/restaurant_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/calendar_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; +import 'package:uni/model/providers/lazy/home_page_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/restaurant_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/about/about.dart'; diff --git a/uni/lib/model/providers/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart similarity index 98% rename from uni/lib/model/providers/bus_stop_provider.dart rename to uni/lib/model/providers/lazy/bus_stop_provider.dart index f4b126fba..9a160960b 100644 --- a/uni/lib/model/providers/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -15,6 +15,8 @@ class BusStopProvider extends StateProviderNotifier { Map _configuredBusStops = Map.identity(); DateTime _timeStamp = DateTime.now(); + BusStopProvider() : super(dependsOnSession: false); + UnmodifiableMapView get configuredBusStops => UnmodifiableMapView(_configuredBusStops); diff --git a/uni/lib/model/providers/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart similarity index 96% rename from uni/lib/model/providers/calendar_provider.dart rename to uni/lib/model/providers/lazy/calendar_provider.dart index add1e8a07..544495198 100644 --- a/uni/lib/model/providers/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -13,6 +13,8 @@ import 'package:uni/model/request_status.dart'; class CalendarProvider extends StateProviderNotifier { List _calendar = []; + CalendarProvider() : super(dependsOnSession: true); + UnmodifiableListView get calendar => UnmodifiableListView(_calendar); diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart similarity index 98% rename from uni/lib/model/providers/exam_provider.dart rename to uni/lib/model/providers/lazy/exam_provider.dart index 0f304f6f8..ec3bfed2c 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -19,6 +19,8 @@ class ExamProvider extends StateProviderNotifier { List _hiddenExams = []; Map _filteredExamsTypes = {}; + ExamProvider() : super(dependsOnSession: true); + UnmodifiableListView get exams => UnmodifiableListView(_exams); UnmodifiableListView get hiddenExams => diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart similarity index 93% rename from uni/lib/model/providers/faculty_locations_provider.dart rename to uni/lib/model/providers/lazy/faculty_locations_provider.dart index 6d9468272..fcd89108d 100644 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -10,6 +10,8 @@ import 'package:uni/model/request_status.dart'; class FacultyLocationsProvider extends StateProviderNotifier { List _locations = []; + FacultyLocationsProvider() : super(dependsOnSession: false); + UnmodifiableListView get locations => UnmodifiableListView(_locations); diff --git a/uni/lib/model/providers/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart similarity index 94% rename from uni/lib/model/providers/home_page_provider.dart rename to uni/lib/model/providers/lazy/home_page_provider.dart index 8fe189b80..483ce5b57 100644 --- a/uni/lib/model/providers/home_page_provider.dart +++ b/uni/lib/model/providers/lazy/home_page_provider.dart @@ -1,14 +1,17 @@ +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/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/utils/favorite_widget_type.dart'; class HomePageProvider extends StateProviderNotifier { List _favoriteCards = []; bool _isEditing = false; + HomePageProvider() : super(dependsOnSession: false); + List get favoriteCards => _favoriteCards.toList(); + bool get isEditing => _isEditing; @override diff --git a/uni/lib/model/providers/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart similarity index 98% rename from uni/lib/model/providers/lecture_provider.dart rename to uni/lib/model/providers/lazy/lecture_provider.dart index 177a4bb61..0d1408c4c 100644 --- a/uni/lib/model/providers/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -17,6 +17,8 @@ import 'package:uni/model/request_status.dart'; class LectureProvider extends StateProviderNotifier { List _lectures = []; + LectureProvider() : super(dependsOnSession: true); + UnmodifiableListView get lectures => UnmodifiableListView(_lectures); @override diff --git a/uni/lib/model/providers/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart similarity index 96% rename from uni/lib/model/providers/library_occupation_provider.dart rename to uni/lib/model/providers/lazy/library_occupation_provider.dart index 6e0f58fb0..bd7d09517 100644 --- a/uni/lib/model/providers/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -12,6 +12,8 @@ import 'package:uni/model/request_status.dart'; class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupation? _occupation; + LibraryOccupationProvider() : super(dependsOnSession: true); + LibraryOccupation? get occupation => _occupation; @override diff --git a/uni/lib/model/providers/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart similarity index 96% rename from uni/lib/model/providers/restaurant_provider.dart rename to uni/lib/model/providers/lazy/restaurant_provider.dart index d6a7a7556..52b1c9a6b 100644 --- a/uni/lib/model/providers/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -13,6 +13,8 @@ import 'package:uni/model/request_status.dart'; class RestaurantProvider extends StateProviderNotifier { List _restaurants = []; + RestaurantProvider() : super(dependsOnSession: false); + UnmodifiableListView get restaurants => UnmodifiableListView(_restaurants); diff --git a/uni/lib/model/providers/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart similarity index 99% rename from uni/lib/model/providers/profile_provider.dart rename to uni/lib/model/providers/startup/profile_provider.dart index 456dcd781..a101e41db 100644 --- a/uni/lib/model/providers/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -26,6 +26,8 @@ class ProfileProvider extends StateProviderNotifier { DateTime? _feesRefreshTime; DateTime? _printRefreshTime; + ProfileProvider() : super(dependsOnSession: true); + String get feesRefreshTime => _feesRefreshTime.toString(); String get printRefreshTime => _printRefreshTime.toString(); diff --git a/uni/lib/model/providers/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart similarity index 74% rename from uni/lib/model/providers/session_provider.dart rename to uni/lib/model/providers/startup/session_provider.dart index fe334db14..296f70ccc 100644 --- a/uni/lib/model/providers/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -10,13 +10,14 @@ import 'package:uni/model/entities/login_exceptions.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/providers/state_providers.dart'; import 'package:uni/model/request_status.dart'; class SessionProvider extends StateProviderNotifier { Session _session = Session(); List _faculties = []; + SessionProvider() : super(dependsOnSession: false); + Session get session => _session; UnmodifiableListView get faculties => @@ -28,14 +29,8 @@ class SessionProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async {} - login(Completer action, - String username, - String password, - List faculties, - StateProviders stateProviders, - persistentSession, - usernameController, - passwordController) async { + login(Completer action, String username, String password, + List faculties, persistentSession) async { try { updateStatus(RequestStatus.busy); @@ -49,19 +44,14 @@ class SessionProvider extends StateProviderNotifier { username, password, faculties); } Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); - - //loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); - //await loadUserProfileInfoFromRemote(stateProviders); + () => {NotificationManager().initializeNotifications()}); - usernameController.clear(); - passwordController.clear(); await acceptTermsAndConditions(); updateStatus(RequestStatus.successful); } else { final String responseHtml = - await NetworkRouter.loginInSigarra(username, password, faculties); + await NetworkRouter.loginInSigarra(username, password, faculties); if (isPasswordExpired(responseHtml)) { action.completeError(ExpiredCredentialsException()); } else { @@ -80,22 +70,18 @@ class SessionProvider extends StateProviderNotifier { } reLogin(String username, String password, List faculties, - StateProviders stateProviders, {Completer? action}) async { try { - //loadLocalUserInfoToState(stateProviders); updateStatus(RequestStatus.busy); _session = await NetworkRouter.login(username, password, faculties, true); - //notifyListeners(); if (session.authenticated) { - //await loadUserProfileInfoFromRemote(stateProviders); Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); updateStatus(RequestStatus.successful); action?.complete(); } else { - failRelogin(action); + failReLogin(action); } } catch (e) { _session = Session( @@ -106,11 +92,11 @@ class SessionProvider extends StateProviderNotifier { cookies: '', persistentSession: true); - failRelogin(action); + failReLogin(action); } } - void failRelogin(Completer? action) { + void failReLogin(Completer? action) { notifyListeners(); updateStatus(RequestStatus.failed); action?.completeError(RequestStatus.failed); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 54afd3938..892b81216 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,29 +1,44 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:synchronized/synchronized.dart'; 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/profile_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { + static final Lock _lock = Lock(); RequestStatus _status = RequestStatus.none; bool _initialized = false; DateTime? _lastUpdateTime; + bool dependsOnSession; RequestStatus get status => _status; DateTime? get lastUpdateTime => _lastUpdateTime; + StateProviderNotifier({required this.dependsOnSession}); + Future _loadFromRemote(Session session, Profile profile) async { - if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - await loadFromRemote(session, profile); - _lastUpdateTime = DateTime.now(); - await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); + if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { + return; + } + + updateStatus(RequestStatus.busy); + + await loadFromRemote(session, profile); + + if (_status == RequestStatus.busy) { + // No online activity from provider + updateStatus(RequestStatus.successful); } + + _lastUpdateTime = DateTime.now(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); } void updateStatus(RequestStatus status) { @@ -37,34 +52,36 @@ abstract class StateProviderNotifier extends ChangeNotifier { final profile = Provider.of(context, listen: false).profile; - updateStatus(RequestStatus.busy); _loadFromRemote(session, profile); } Future ensureInitialized(Session session, Profile profile) async { - if (_initialized) { - return; - } + await _lock.synchronized(() async { + if (_initialized) { + return; + } - _initialized = true; + _initialized = true; - _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( - runtimeType.toString()); + _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( + runtimeType.toString()); - updateStatus(RequestStatus.busy); + updateStatus(RequestStatus.busy); - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - final sessionIsPersistent = - userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; - if (sessionIsPersistent) { - await loadFromStorage(); - if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { - updateStatus(RequestStatus.none); + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + final sessionIsPersistent = + userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; + if (sessionIsPersistent) { + await loadFromStorage(); + if (await Connectivity().checkConnectivity() == + ConnectivityResult.none) { + updateStatus(RequestStatus.none); + } } - } - _loadFromRemote(session, profile); + await _loadFromRemote(session, profile); + }); notifyListeners(); } diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index 4262f541e..f93055941 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -1,15 +1,15 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; -import 'package:uni/model/providers/calendar_provider.dart'; -import 'package:uni/model/providers/exam_provider.dart'; -import 'package:uni/model/providers/faculty_locations_provider.dart'; -import 'package:uni/model/providers/home_page_provider.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; -import 'package:uni/model/providers/library_occupation_provider.dart'; -import 'package:uni/model/providers/profile_provider.dart'; -import 'package:uni/model/providers/restaurant_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/calendar_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; +import 'package:uni/model/providers/lazy/home_page_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/restaurant_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; class StateProviders { final LectureProvider lectureProvider; diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 60677bc01..56366263c 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index a468aa2ad..d42df38a1 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/view/lazy_consumer.dart'; /// Manages the section with the estimated time for the bus arrival diff --git a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart index ad4e1e5ad..cc3af9b83 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/view/bus_stop_selection/widgets/bus_stop_search.dart'; import 'package:uni/view/bus_stop_selection/widgets/bus_stop_selection_row.dart'; import 'package:uni/view/common_widgets/page_title.dart'; diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart index 7402800a7..86a619b67 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart @@ -6,7 +6,7 @@ import 'package:provider/provider.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/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/view/bus_stop_selection/widgets/form.dart'; /// Manages the section of the app displayed when the diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart index 6c26e7fe8..4d09758b7 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/view/common_widgets/row_container.dart'; class BusStopSelectionRow extends StatefulWidget { diff --git a/uni/lib/view/bus_stop_selection/widgets/form.dart b/uni/lib/view/bus_stop_selection/widgets/form.dart index 6760dcc20..9c00f68b4 100644 --- a/uni/lib/view/bus_stop_selection/widgets/form.dart +++ b/uni/lib/view/bus_stop_selection/widgets/form.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:uni/controller/fetchers/departures_fetcher.dart'; import 'package:uni/model/entities/bus.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; class BusesForm extends StatefulWidget { final String stopCode; diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 6b341be61..64fa8caff 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:timelines/timelines.dart'; import 'package:uni/model/entities/calendar_event.dart'; -import 'package:uni/model/providers/calendar_provider.dart'; +import 'package:uni/model/providers/lazy/calendar_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; 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 fde15635f..84325ea06 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/load_info.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart'; import 'package:uni/view/profile/profile.dart'; diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart index cacfd953c..192ad6f43 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/theme_notifier.dart'; diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index d9be172e3..b4b3d4bd7 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/course_unit.dart'; -import 'package:uni/model/providers/profile_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 9413971dc..d2821097c 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/day_title.dart'; diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 766092afa..613cadecf 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; class ExamFilterForm extends StatefulWidget { final Map filteredExamsTypes; diff --git a/uni/lib/view/exams/widgets/exam_filter_menu.dart b/uni/lib/view/exams/widgets/exam_filter_menu.dart index bf747379a..0576ebe67 100644 --- a/uni/lib/view/exams/widgets/exam_filter_menu.dart +++ b/uni/lib/view/exams/widgets/exam_filter_menu.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/widgets/exam_filter_form.dart'; class ExamFilterMenu extends StatefulWidget { diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index 21fbc1347..ca3c13483 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -1,13 +1,13 @@ import 'dart:async'; +import 'package:add_2_calendar/add_2_calendar.dart'; import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; -import 'package:uni/view/exams/widgets/exam_title.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/widgets/exam_time.dart'; -import 'package:add_2_calendar/add_2_calendar.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:uni/view/exams/widgets/exam_title.dart'; class ExamRow extends StatefulWidget { final Exam exam; diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index b2f173e12..6a0c93a78 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; -import 'package:uni/model/providers/exam_provider.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; -import 'package:uni/model/providers/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/model/providers/lazy/home_page_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; -import '../../model/providers/home_page_provider.dart'; -import '../../model/providers/profile_provider.dart'; - class HomePageView extends StatefulWidget { const HomePageView({super.key}); diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 7097b8e7f..0f981093d 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 9b2285100..92c10ba38 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 11510c610..bf4156ae9 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/model/providers/home_page_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/lazy/home_page_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 34c892dcc..39c440043 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/restaurant_provider.dart'; +import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index 414d1c9e9..d0f066ec8 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index bcde3ae1f..bc3cd460a 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -1,14 +1,14 @@ import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/profile_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; /// Wrapper around Consumer that ensures that the provider is initialized, /// meaning that it has loaded its data from storage and/or remote. /// The provider will not reload its data if it has already been loaded before. -/// There should be a SessionProvider and a ProfileProvider above this widget in -/// the widget tree to initialize the provider data the first time. +/// If the provider depends on the session, it will ensure that SessionProvider +/// and ProfileProvider are initialized before initializing itself. class LazyConsumer extends StatelessWidget { final Widget Function(BuildContext, T) builder; @@ -23,12 +23,19 @@ class LazyConsumer extends StatelessWidget { final sessionProvider = Provider.of(context); final profileProvider = Provider.of(context); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { final session = sessionProvider.session; final profile = profileProvider.profile; - profileProvider.ensureInitialized(session, profile).then((value) => - Provider.of(context, listen: false) - .ensureInitialized(session, profile)); + final provider = Provider.of(context, listen: false); + + if (provider.dependsOnSession) { + sessionProvider.ensureInitialized(session, profile).then((_) => + profileProvider + .ensureInitialized(session, profile) + .then((_) => provider.ensureInitialized(session, profile))); + } else { + provider.ensureInitialized(session, profile); + } }); } catch (_) { // The provider won't be initialized diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 9025266e8..c9581b9ef 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/library_occupation.dart'; -import 'package:uni/model/providers/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/lazy_consumer.dart'; diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index d4e9c6027..b4af4b85f 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/percent_indicator.dart'; -import 'package:uni/model/providers/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index 956c636be..a64ff5f6a 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:uni/model/entities/location_group.dart'; -import 'package:uni/model/providers/faculty_locations_provider.dart'; +import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index f0395c19f..848247506 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/login_exceptions.dart'; -import 'package:uni/model/providers/session_provider.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'; @@ -54,8 +54,7 @@ class LoginPageViewState extends State { final pass = passwordController.text.trim(); final completer = Completer(); - sessionProvider.login(completer, user, pass, faculties, stateProviders, - _keepSignedIn, usernameController, passwordController); + sessionProvider.login(completer, user, pass, faculties, _keepSignedIn); completer.future.then((_) { handleLogin(sessionProvider.status, context); diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 97bff9f00..8770b82b1 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/profile_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/account_info_card.dart'; diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index c759692b9..018ce766d 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/profile_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/tuition_notification_switch.dart'; diff --git a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart index c88dd2acb..5ce0adf4b 100644 --- a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart +++ b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart @@ -2,7 +2,7 @@ import 'package:currency_text_input_formatter/currency_text_input_formatter.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/fetchers/print_fetcher.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; Future addMoneyDialog(BuildContext context) async { diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index 0ae056c15..bb41169b5 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/model/providers/profile_provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/create_print_mb_dialog.dart'; diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index f59f30574..a41d5415d 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; class ProfileOverview extends StatelessWidget { final Profile profile; diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index b568a82a6..f72d0de0f 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; -import 'package:uni/model/providers/restaurant_provider.dart'; +import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/model/utils/day_of_week.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 0831d96d0..1cc4b9d65 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index 130b469fd..5b0ad0d22 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -132,7 +132,7 @@ class SplashScreenState extends State { final List faculties = await AppSharedPreferences.getUserFaculties(); await stateProviders.sessionProvider - .reLogin(userName, password, faculties, stateProviders); + .reLogin(userName, password, faculties); } return MaterialPageRoute(builder: (context) => const HomePageView()); diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index f3d83b34d..74ca18090 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -16,7 +16,7 @@ import 'package:uni/model/entities/course_unit.dart'; 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/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/exams.dart'; import '../../test_widget.dart'; diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index d4ffa5e8c..7451d4b49 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -14,7 +14,7 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/view/schedule/schedule.dart'; import '../../test_widget.dart'; diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index ad0b4964a..1050ea09e 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -11,7 +11,7 @@ import 'package:uni/model/entities/course_unit.dart'; 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/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/model/request_status.dart'; import 'mocks.dart'; diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index e9dcee38f..212e68767 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -10,7 +10,7 @@ import 'package:uni/model/entities/course.dart'; 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/lecture_provider.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/request_status.dart'; import 'mocks.dart'; diff --git a/uni/test/unit/view/Pages/exams_page_view_test.dart b/uni/test/unit/view/Pages/exams_page_view_test.dart index 5e3a8af34..5cfbc1040 100644 --- a/uni/test/unit/view/Pages/exams_page_view_test.dart +++ b/uni/test/unit/view/Pages/exams_page_view_test.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; - import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/exams.dart'; import '../../../test_widget.dart'; @@ -17,16 +16,13 @@ void main() { const firstExamDate = '2019-09-11'; const secondExamSubject = 'SDIS'; const secondExamDate = '2019-09-12'; - - testWidgets('When given an empty list', (WidgetTester tester) async { + testWidgets('When given an empty list', (WidgetTester tester) async { const widget = ExamsPageView(); final examProvider = ExamProvider(); examProvider.setExams([]); - final providers = [ - ChangeNotifierProvider(create: (_) => examProvider) - ]; + final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); @@ -34,19 +30,17 @@ void main() { }); testWidgets('When given a single exam', (WidgetTester tester) async { - final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); + final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1230',firstExamBegin, firstExamEnd, firstExamSubject, - ['B119', 'B107', 'B205'], 'ER','feup'); + final firstExam = Exam('1230', firstExamBegin, firstExamEnd, + firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); const widget = ExamsPageView(); final examProvider = ExamProvider(); examProvider.setExams([firstExam]); - final providers = [ - ChangeNotifierProvider(create: (_) => examProvider) - ]; + final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); @@ -58,12 +52,12 @@ void main() { (WidgetTester tester) async { final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1231',firstExamBegin, firstExamEnd, firstExamSubject, - ['B119', 'B107', 'B205'], 'ER', 'feup'); + final firstExam = Exam('1231', firstExamBegin, firstExamEnd, + firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); final DateTime secondExamBegin = DateTime.parse('$firstExamDate 12:00'); final DateTime secondExamEnd = DateTime.parse('$firstExamDate 15:00'); - final secondExam = Exam('1232',secondExamBegin, secondExamEnd, secondExamSubject, - ['B119', 'B107', 'B205'], 'ER', 'feup'); + final secondExam = Exam('1232', secondExamBegin, secondExamEnd, + secondExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); final examList = [ firstExam, @@ -75,9 +69,7 @@ void main() { final examProvider = ExamProvider(); examProvider.setExams(examList); - final providers = [ - ChangeNotifierProvider(create: (_) => examProvider) - ]; + final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); @@ -91,12 +83,12 @@ void main() { (WidgetTester tester) async { final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1233',firstExamBegin, firstExamEnd, firstExamSubject, - ['B119', 'B107', 'B205'], 'ER','feup'); + final firstExam = Exam('1233', firstExamBegin, firstExamEnd, + firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); final DateTime secondExamBegin = DateTime.parse('$secondExamDate 12:00'); final DateTime secondExamEnd = DateTime.parse('$secondExamDate 15:00'); - final secondExam = Exam('1234',secondExamBegin, secondExamEnd, secondExamSubject, - ['B119', 'B107', 'B205'], 'ER','feup'); + final secondExam = Exam('1234', secondExamBegin, secondExamEnd, + secondExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); final examList = [ firstExam, secondExam, @@ -107,9 +99,7 @@ void main() { final examProvider = ExamProvider(); examProvider.setExams(examList); - final providers = [ - ChangeNotifierProvider(create: (_) => examProvider) - ]; + final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); expect(find.byKey(Key(firstExam.toString())), findsOneWidget); @@ -123,20 +113,20 @@ void main() { final List rooms = ['B119', 'B107', 'B205']; final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1235',firstExamBegin, firstExamEnd, firstExamSubject, - rooms, 'ER', 'feup'); + final firstExam = Exam('1235', firstExamBegin, firstExamEnd, + firstExamSubject, rooms, 'ER', 'feup'); final DateTime secondExamBegin = DateTime.parse('$firstExamDate 10:00'); final DateTime secondExamEnd = DateTime.parse('$firstExamDate 12:00'); - final secondExam = Exam('1236',secondExamBegin, secondExamEnd, firstExamSubject, - rooms, 'ER', 'feup'); + final secondExam = Exam('1236', secondExamBegin, secondExamEnd, + firstExamSubject, rooms, 'ER', 'feup'); final DateTime thirdExamBegin = DateTime.parse('$secondExamDate 12:00'); final DateTime thirdExamEnd = DateTime.parse('$secondExamDate 15:00'); - final thirdExam = Exam('1237',thirdExamBegin, thirdExamEnd, secondExamSubject, - rooms, 'ER', 'feup'); + final thirdExam = Exam('1237', thirdExamBegin, thirdExamEnd, + secondExamSubject, rooms, 'ER', 'feup'); final DateTime fourthExamBegin = DateTime.parse('$secondExamDate 13:00'); final DateTime fourthExamEnd = DateTime.parse('$secondExamDate 14:00'); - final fourthExam = Exam('1238',fourthExamBegin, fourthExamEnd, secondExamSubject, - rooms, 'ER', 'feup'); + final fourthExam = Exam('1238', fourthExamBegin, fourthExamEnd, + secondExamSubject, rooms, 'ER', 'feup'); final examList = [firstExam, secondExam, thirdExam, fourthExam]; const widget = ExamsPageView(); @@ -149,9 +139,7 @@ void main() { final secondDayKey = [thirdExam, fourthExam].map((ex) => ex.toString()).join(); - final providers = [ - ChangeNotifierProvider(create: (_) => examProvider) - ]; + final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); expect(find.byKey(Key(firstDayKey)), findsOneWidget); diff --git a/uni/test/unit/view/Widgets/exam_row_test.dart b/uni/test/unit/view/Widgets/exam_row_test.dart index 79dbe3a24..62459a32f 100644 --- a/uni/test/unit/view/Widgets/exam_row_test.dart +++ b/uni/test/unit/view/Widgets/exam_row_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/exam_provider.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; import '../../../test_widget.dart'; @@ -37,7 +37,7 @@ void main() { testWidgets('When multiple rooms', (WidgetTester tester) async { final rooms = ['B315', 'B316', 'B330']; - final Exam exam = Exam('1230',begin, end, subject, rooms, '', 'feup'); + final Exam exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); final widget = ExamRow(exam: exam, teacher: '', mainPage: true); final providers = [ From c7c6854cbda38dde0ef6b15849ea79395c2f10b7 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 10 Jul 2023 15:21:09 +0100 Subject: [PATCH 041/100] Refine request status logic --- .../providers/startup/profile_provider.dart | 24 ++++---- .../providers/state_provider_notifier.dart | 57 +++++++++---------- uni/lib/view/course_units/course_units.dart | 9 ++- .../unit/providers/exams_provider_test.dart | 2 +- .../unit/providers/lecture_provider_test.dart | 2 +- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index a101e41db..dd7d42b81 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -36,6 +36,7 @@ class ProfileProvider extends StateProviderNotifier { @override Future loadFromStorage() async { + await loadProfile(); await Future.wait( [loadCourses(), loadBalanceRefreshTimes(), loadCourseUnits()]); } @@ -62,20 +63,21 @@ class ProfileProvider extends StateProviderNotifier { ]); } - Future loadCourses() async { + Future loadProfile() async { final profileDb = AppUserDataDatabase(); _profile = await profileDb.getUserData(); + } + Future loadCourses() async { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); final List courses = await coursesDb.courses(); - _profile.courses = courses; } Future loadBalanceRefreshTimes() async { final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); final Map refreshTimes = - await refreshTimesDb.refreshTimes(); + await refreshTimesDb.refreshTimes(); final printRefreshTime = refreshTimes['print']; final feesRefreshTime = refreshTimes['fees']; @@ -101,7 +103,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); @@ -130,7 +132,7 @@ class ProfileProvider extends StateProviderNotifier { Future storeRefreshTime(String db, String currentTime) async { final AppRefreshTimesDatabase refreshTimesDatabase = - AppRefreshTimesDatabase(); + AppRefreshTimesDatabase(); refreshTimesDatabase.saveRefreshTime(db, currentTime); } @@ -141,7 +143,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); @@ -174,7 +176,7 @@ class ProfileProvider extends StateProviderNotifier { final profile = await ProfileFetcher.getProfile(session); final currentCourseUnits = - await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); + await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); _profile = profile; _profile.courseUnits = currentCourseUnits; @@ -182,7 +184,7 @@ class ProfileProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final profileDb = AppUserDataDatabase(); profileDb.insertUserData(_profile); @@ -195,8 +197,8 @@ class ProfileProvider extends StateProviderNotifier { action.complete(); } - fetchCourseUnitsAndCourseAverages( - Session session, Completer action) async { + fetchCourseUnitsAndCourseAverages(Session session, + Completer action) async { updateStatus(RequestStatus.busy); try { final List courses = profile.courses; @@ -206,7 +208,7 @@ class ProfileProvider extends StateProviderNotifier { _profile.courseUnits = allCourseUnits; final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); await coursesDb.saveNewCourses(courses); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 892b81216..ecf5594f6 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -11,7 +11,7 @@ import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { static final Lock _lock = Lock(); - RequestStatus _status = RequestStatus.none; + RequestStatus _status = RequestStatus.busy; bool _initialized = false; DateTime? _lastUpdateTime; bool dependsOnSession; @@ -22,23 +22,36 @@ abstract class StateProviderNotifier extends ChangeNotifier { StateProviderNotifier({required this.dependsOnSession}); - Future _loadFromRemote(Session session, Profile profile) async { - if (await Connectivity().checkConnectivity() == ConnectivityResult.none) { - return; - } + Future _loadFromStorage() async { + _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( + runtimeType.toString()); - updateStatus(RequestStatus.busy); + final userPersistentInfo = + await AppSharedPreferences.getPersistentUserInfo(); + final sessionIsPersistent = + userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; + if (sessionIsPersistent) { + await loadFromStorage(); + } + } - await loadFromRemote(session, profile); + Future _loadFromRemote(Session session, Profile profile) async { + final bool hasConnectivity = + await Connectivity().checkConnectivity() != ConnectivityResult.none; + if (hasConnectivity) { + updateStatus(RequestStatus.busy); + await loadFromRemote(session, profile); + } - if (_status == RequestStatus.busy) { + if (!hasConnectivity || _status == RequestStatus.busy) { // No online activity from provider updateStatus(RequestStatus.successful); + } else { + _lastUpdateTime = DateTime.now(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); + notifyListeners(); } - - _lastUpdateTime = DateTime.now(); - await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); } void updateStatus(RequestStatus status) { @@ -63,27 +76,9 @@ abstract class StateProviderNotifier extends ChangeNotifier { _initialized = true; - _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( - runtimeType.toString()); - - updateStatus(RequestStatus.busy); - - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - final sessionIsPersistent = - userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; - if (sessionIsPersistent) { - await loadFromStorage(); - if (await Connectivity().checkConnectivity() == - ConnectivityResult.none) { - updateStatus(RequestStatus.none); - } - } - + await _loadFromStorage(); await _loadFromRemote(session, profile); }); - - notifyListeners(); } Future loadFromStorage(); diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index b4b3d4bd7..165787618 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -33,6 +33,7 @@ class CourseUnitsPageViewState final List courseUnits = profileProvider.profile.courseUnits; List availableYears = []; List availableSemesters = []; + if (courseUnits.isNotEmpty) { availableYears = _getAvailableYears(courseUnits); if (availableYears.isNotEmpty && selectedSchoolYear == null) { @@ -52,12 +53,10 @@ class CourseUnitsPageViewState ? availableSemesters[0] : availableSemesters[1]; } - - return _getPageView(courseUnits, profileProvider.status, availableYears, - availableSemesters); - } else { - return _getPageView([], profileProvider.status, [], []); } + + return _getPageView(courseUnits, profileProvider.status, availableYears, + availableSemesters); }); } diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 1050ea09e..fb94541a9 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -59,7 +59,7 @@ void main() { setUp(() { provider = ExamProvider(); - expect(provider.status, RequestStatus.none); + expect(provider.status, RequestStatus.busy); }); test('When given one exam', () async { diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 212e68767..d05496119 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -39,7 +39,7 @@ void main() { LectureProvider provider; setUp(() { provider = LectureProvider(); - expect(provider.status, RequestStatus.none); + expect(provider.status, RequestStatus.busy); }); test('When given a single schedule', () async { From 5fa3a75270bc0d921e4c211e1e9b6ab5c13d8138 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 10 Jul 2023 15:29:05 +0100 Subject: [PATCH 042/100] Delete load info file --- uni/lib/controller/load_info.dart | 16 --------- .../providers/startup/profile_provider.dart | 34 ++++++++++++++----- .../pages_layouts/general/general.dart | 11 +++--- .../profile/widgets/profile_overview.dart | 5 +-- 4 files changed, 34 insertions(+), 32 deletions(-) delete mode 100644 uni/lib/controller/load_info.dart diff --git a/uni/lib/controller/load_info.dart b/uni/lib/controller/load_info.dart deleted file mode 100644 index 92fe751c4..000000000 --- a/uni/lib/controller/load_info.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:uni/controller/local_storage/file_offline_storage.dart'; -import 'package:uni/model/entities/session.dart'; - -Future loadProfilePicture(Session session, {forceRetrieval = false}) { - final String studentNumber = session.studentNumber; - final String faculty = session.faculties[0]; - final String url = - 'https://sigarra.up.pt/$faculty/pt/fotografias_service.foto?pct_cod=$studentNumber'; - final Map headers = {}; - headers['cookie'] = session.cookies; - return loadFileFromStorageOrRetrieveNew('user_profile_picture', url, headers, - forceRetrieval: forceRetrieval); -} diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index dd7d42b81..bcb7f1984 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; @@ -21,6 +22,8 @@ import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; +import '../../../controller/local_storage/file_offline_storage.dart'; + class ProfileProvider extends StateProviderNotifier { Profile _profile = Profile(); DateTime? _feesRefreshTime; @@ -77,7 +80,7 @@ class ProfileProvider extends StateProviderNotifier { Future loadBalanceRefreshTimes() async { final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); final Map refreshTimes = - await refreshTimesDb.refreshTimes(); + await refreshTimesDb.refreshTimes(); final printRefreshTime = refreshTimes['print']; final feesRefreshTime = refreshTimes['fees']; @@ -103,7 +106,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); @@ -132,7 +135,7 @@ class ProfileProvider extends StateProviderNotifier { Future storeRefreshTime(String db, String currentTime) async { final AppRefreshTimesDatabase refreshTimesDatabase = - AppRefreshTimesDatabase(); + AppRefreshTimesDatabase(); refreshTimesDatabase.saveRefreshTime(db, currentTime); } @@ -143,7 +146,7 @@ class ProfileProvider extends StateProviderNotifier { final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); @@ -176,7 +179,7 @@ class ProfileProvider extends StateProviderNotifier { final profile = await ProfileFetcher.getProfile(session); final currentCourseUnits = - await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); + await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); _profile = profile; _profile.courseUnits = currentCourseUnits; @@ -184,7 +187,7 @@ class ProfileProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final profileDb = AppUserDataDatabase(); profileDb.insertUserData(_profile); @@ -197,8 +200,8 @@ class ProfileProvider extends StateProviderNotifier { action.complete(); } - fetchCourseUnitsAndCourseAverages(Session session, - Completer action) async { + fetchCourseUnitsAndCourseAverages( + Session session, Completer action) async { updateStatus(RequestStatus.busy); try { final List courses = profile.courses; @@ -208,7 +211,7 @@ class ProfileProvider extends StateProviderNotifier { _profile.courseUnits = allCourseUnits; final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); await coursesDb.saveNewCourses(courses); @@ -223,4 +226,17 @@ class ProfileProvider extends StateProviderNotifier { action.complete(); } + + static Future fetchOrGetCachedProfilePicture(Session session, + {forceRetrieval = false}) { + final String studentNumber = session.studentNumber; + final String faculty = session.faculties[0]; + final String url = + 'https://sigarra.up.pt/$faculty/pt/fotografias_service.foto?pct_cod=$studentNumber'; + final Map headers = {}; + headers['cookie'] = session.cookies; + return loadFileFromStorageOrRetrieveNew( + 'user_profile_picture', url, headers, + forceRetrieval: forceRetrieval); + } } 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 84325ea06..9ebaac4bf 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; -import 'package:uni/controller/load_info.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'; import 'package:uni/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart'; @@ -28,9 +28,10 @@ abstract class GeneralPageViewState extends State { Future buildProfileDecorationImage(context, {forceRetrieval = false}) async { - final profilePictureFile = await loadProfilePicture( - Provider.of(context, listen: false).session, - forceRetrieval: forceRetrieval || profileImageProvider == null); + final profilePictureFile = + await ProfileProvider.fetchOrGetCachedProfilePicture( + Provider.of(context, listen: false).session, + forceRetrieval: forceRetrieval || profileImageProvider == null); return getProfileDecorationImage(profilePictureFile); } @@ -53,7 +54,7 @@ abstract class GeneralPageViewState extends State { Widget refreshState(BuildContext context, Widget child) { return RefreshIndicator( key: GlobalKey(), - onRefresh: () => loadProfilePicture( + onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture( Provider.of(context, listen: false).session, forceRetrieval: true) .then((value) => handleRefresh(context)), diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index a41d5415d..5382f7006 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/controller/load_info.dart'; import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; class ProfileOverview extends StatelessWidget { @@ -21,7 +21,8 @@ class ProfileOverview extends StatelessWidget { return Consumer( builder: (context, sessionProvider, _) { return FutureBuilder( - future: loadProfilePicture(sessionProvider.session), + future: ProfileProvider.fetchOrGetCachedProfilePicture( + sessionProvider.session), builder: (BuildContext context, AsyncSnapshot profilePic) => Column( mainAxisAlignment: MainAxisAlignment.center, From 1d13aa335b357b7493ee6f636390f277553dc064 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 10 Jul 2023 15:40:44 +0100 Subject: [PATCH 043/100] Delete onstartup file --- .../controller/networking/network_router.dart | 16 +++++++--------- uni/lib/controller/on_start_up.dart | 18 ------------------ uni/lib/main.dart | 2 -- .../providers/startup/profile_provider.dart | 3 +-- .../providers/startup/session_provider.dart | 14 +++++++------- 5 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 uni/lib/controller/on_start_up.dart diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index c6bba5b61..f92b85e7b 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -7,6 +7,7 @@ import 'package:logger/logger.dart'; import 'package:synchronized/synchronized.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/entities/session.dart'; +import 'package:uni/view/navigation_service.dart'; extension UriString on String { /// Converts a [String] to an [Uri]. @@ -16,13 +17,9 @@ extension UriString on String { /// Manages the networking of the app. class NetworkRouter { static http.Client? httpClient; - static const int loginRequestTimeout = 20; - static Lock loginLock = Lock(); - static Function onReloginFail = () {}; - /// Creates an authenticated [Session] on the given [faculty] with the /// given username [user] and password [pass]. static Future login(String user, String pass, List faculties, @@ -52,7 +49,7 @@ class NetworkRouter { } /// Determines if a re-login with the [session] is possible. - static Future relogin(Session session) { + static Future reLogin(Session session) { return loginLock.synchronized(() async { if (!session.persistentSession) { return false; @@ -94,10 +91,11 @@ class NetworkRouter { /// Returns the response body of the login in Sigarra /// given username [user] and password [pass]. - static Future loginInSigarra(String user, String pass, List faculties) async { + static Future loginInSigarra( + String user, String pass, List faculties) async { final String url = '${NetworkRouter.getBaseUrls(faculties)[0]}vld_validacao.validacao'; - + final response = await http.post(url.toUri(), body: { 'p_user': user, 'p_pass': pass @@ -149,12 +147,12 @@ class NetworkRouter { return response; } else if (response.statusCode == 403 && !(await userLoggedIn(session))) { // HTTP403 - Forbidden - final bool reLoginSuccessful = await relogin(session); + final bool reLoginSuccessful = await reLogin(session); if (reLoginSuccessful) { headers['cookie'] = session.cookies; return http.get(url.toUri(), headers: headers); } else { - onReloginFail(); + NavigationService.logout(); Logger().e('Login failed'); return Future.error('Login failed'); } diff --git a/uni/lib/controller/on_start_up.dart b/uni/lib/controller/on_start_up.dart deleted file mode 100644 index f51e5a032..000000000 --- a/uni/lib/controller/on_start_up.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/model/providers/startup/session_provider.dart'; -import 'package:uni/view/navigation_service.dart'; - -class OnStartUp { - static onStart(SessionProvider sessionProvider) { - setHandleReloginFail(sessionProvider); - } - - static setHandleReloginFail(SessionProvider sessionProvider) { - NetworkRouter.onReloginFail = () { - if (!sessionProvider.session.persistentSession) { - return NavigationService.logout(); - } - return Future.value(); - }; - } -} diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 3b5039fe7..03248f59f 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -7,7 +7,6 @@ import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/background_workers/background_callback.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/controller/on_start_up.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/providers/lazy/calendar_provider.dart'; import 'package:uni/model/providers/lazy/exam_provider.dart'; @@ -57,7 +56,6 @@ Future main() async { FacultyLocationsProvider(), HomePageProvider()); - OnStartUp.onStart(stateProviders.sessionProvider); WidgetsFlutterBinding.ensureInitialized(); await Workmanager().initialize(workerStartCallback, diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index bcb7f1984..f2e48e0dd 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -13,6 +13,7 @@ import 'package:uni/controller/local_storage/app_courses_database.dart'; import 'package:uni/controller/local_storage/app_refresh_times_database.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/app_user_database.dart'; +import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; import 'package:uni/model/entities/course.dart'; @@ -22,8 +23,6 @@ import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; -import '../../../controller/local_storage/file_offline_storage.dart'; - class ProfileProvider extends StateProviderNotifier { Profile _profile = Profile(); DateTime? _feesRefreshTime; diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 296f70ccc..b860c267e 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -11,6 +11,7 @@ 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/view/navigation_service.dart'; class SessionProvider extends StateProviderNotifier { Session _session = Session(); @@ -47,7 +48,6 @@ class SessionProvider extends StateProviderNotifier { () => {NotificationManager().initializeNotifications()}); await acceptTermsAndConditions(); - updateStatus(RequestStatus.successful); } else { final String responseHtml = @@ -81,7 +81,7 @@ class SessionProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); action?.complete(); } else { - failReLogin(action); + handleFailedReLogin(action); } } catch (e) { _session = Session( @@ -92,14 +92,14 @@ class SessionProvider extends StateProviderNotifier { cookies: '', persistentSession: true); - failReLogin(action); + handleFailedReLogin(action); } } - void failReLogin(Completer? action) { - notifyListeners(); - updateStatus(RequestStatus.failed); + handleFailedReLogin(Completer? action) { action?.completeError(RequestStatus.failed); - NetworkRouter.onReloginFail(); + if (!session.persistentSession) { + return NavigationService.logout(); + } } } From a231d078f01b305042c001e3e653582ed23f3c81 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Mon, 10 Jul 2023 16:35:23 +0100 Subject: [PATCH 044/100] Add provider caching --- .../providers/lazy/bus_stop_provider.dart | 2 +- .../providers/lazy/calendar_provider.dart | 3 +- .../model/providers/lazy/exam_provider.dart | 3 +- .../lazy/faculty_locations_provider.dart | 3 +- .../providers/lazy/home_page_provider.dart | 2 +- .../providers/lazy/lecture_provider.dart | 3 +- .../lazy/library_occupation_provider.dart | 3 +- .../providers/lazy/restaurant_provider.dart | 3 +- .../providers/startup/profile_provider.dart | 3 +- .../providers/startup/session_provider.dart | 6 ++- .../providers/state_provider_notifier.dart | 45 +++++++++++++++---- 11 files changed, 58 insertions(+), 18 deletions(-) diff --git a/uni/lib/model/providers/lazy/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart index 9a160960b..628e9dc9d 100644 --- a/uni/lib/model/providers/lazy/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -15,7 +15,7 @@ class BusStopProvider extends StateProviderNotifier { Map _configuredBusStops = Map.identity(); DateTime _timeStamp = DateTime.now(); - BusStopProvider() : super(dependsOnSession: false); + BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); UnmodifiableMapView get configuredBusStops => UnmodifiableMapView(_configuredBusStops); diff --git a/uni/lib/model/providers/lazy/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart index 544495198..1a204c7d8 100644 --- a/uni/lib/model/providers/lazy/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -13,7 +13,8 @@ import 'package:uni/model/request_status.dart'; class CalendarProvider extends StateProviderNotifier { List _calendar = []; - CalendarProvider() : super(dependsOnSession: true); + CalendarProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 30)); UnmodifiableListView get calendar => UnmodifiableListView(_calendar); diff --git a/uni/lib/model/providers/lazy/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart index ec3bfed2c..9b8ae4532 100644 --- a/uni/lib/model/providers/lazy/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -19,7 +19,8 @@ class ExamProvider extends StateProviderNotifier { List _hiddenExams = []; Map _filteredExamsTypes = {}; - ExamProvider() : super(dependsOnSession: true); + ExamProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); UnmodifiableListView get exams => UnmodifiableListView(_exams); diff --git a/uni/lib/model/providers/lazy/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart index fcd89108d..c0f7a57c7 100644 --- a/uni/lib/model/providers/lazy/faculty_locations_provider.dart +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -10,7 +10,8 @@ import 'package:uni/model/request_status.dart'; class FacultyLocationsProvider extends StateProviderNotifier { List _locations = []; - FacultyLocationsProvider() : super(dependsOnSession: false); + FacultyLocationsProvider() + : super(dependsOnSession: false, cacheDuration: const Duration(days: 30)); UnmodifiableListView get locations => UnmodifiableListView(_locations); diff --git a/uni/lib/model/providers/lazy/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart index 483ce5b57..cd5e21247 100644 --- a/uni/lib/model/providers/lazy/home_page_provider.dart +++ b/uni/lib/model/providers/lazy/home_page_provider.dart @@ -8,7 +8,7 @@ class HomePageProvider extends StateProviderNotifier { List _favoriteCards = []; bool _isEditing = false; - HomePageProvider() : super(dependsOnSession: false); + HomePageProvider() : super(dependsOnSession: false, cacheDuration: null); List get favoriteCards => _favoriteCards.toList(); diff --git a/uni/lib/model/providers/lazy/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart index 0d1408c4c..80f2bfd07 100644 --- a/uni/lib/model/providers/lazy/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -17,7 +17,8 @@ import 'package:uni/model/request_status.dart'; class LectureProvider extends StateProviderNotifier { List _lectures = []; - LectureProvider() : super(dependsOnSession: true); + LectureProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 6)); UnmodifiableListView get lectures => UnmodifiableListView(_lectures); diff --git a/uni/lib/model/providers/lazy/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart index bd7d09517..3b2ffed9e 100644 --- a/uni/lib/model/providers/lazy/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -12,7 +12,8 @@ import 'package:uni/model/request_status.dart'; class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupation? _occupation; - LibraryOccupationProvider() : super(dependsOnSession: true); + LibraryOccupationProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); LibraryOccupation? get occupation => _occupation; diff --git a/uni/lib/model/providers/lazy/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart index 52b1c9a6b..e3655483e 100644 --- a/uni/lib/model/providers/lazy/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -13,7 +13,8 @@ import 'package:uni/model/request_status.dart'; class RestaurantProvider extends StateProviderNotifier { List _restaurants = []; - RestaurantProvider() : super(dependsOnSession: false); + RestaurantProvider() + : super(dependsOnSession: false, cacheDuration: const Duration(days: 1)); UnmodifiableListView get restaurants => UnmodifiableListView(_restaurants); diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index f2e48e0dd..69553c698 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -28,7 +28,8 @@ class ProfileProvider extends StateProviderNotifier { DateTime? _feesRefreshTime; DateTime? _printRefreshTime; - ProfileProvider() : super(dependsOnSession: true); + ProfileProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); String get feesRefreshTime => _feesRefreshTime.toString(); diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index b860c267e..4c1346ebc 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -17,7 +17,11 @@ class SessionProvider extends StateProviderNotifier { Session _session = Session(); List _faculties = []; - SessionProvider() : super(dependsOnSession: false); + SessionProvider() + : super( + dependsOnSession: false, + cacheDuration: null, + initialStatus: RequestStatus.none); Session get session => _session; diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index ecf5594f6..31bfa3bdd 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,5 +1,6 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; import 'package:synchronized/synchronized.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; @@ -11,16 +12,21 @@ import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { static final Lock _lock = Lock(); - RequestStatus _status = RequestStatus.busy; + RequestStatus _status; bool _initialized = false; DateTime? _lastUpdateTime; bool dependsOnSession; + Duration? cacheDuration; RequestStatus get status => _status; DateTime? get lastUpdateTime => _lastUpdateTime; - StateProviderNotifier({required this.dependsOnSession}); + StateProviderNotifier( + {required this.dependsOnSession, + required this.cacheDuration, + RequestStatus? initialStatus}) + : _status = initialStatus ?? RequestStatus.busy; Future _loadFromStorage() async { _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( @@ -30,20 +36,43 @@ abstract class StateProviderNotifier extends ChangeNotifier { await AppSharedPreferences.getPersistentUserInfo(); final sessionIsPersistent = userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; + if (sessionIsPersistent) { await loadFromStorage(); + Logger().i("Loaded $runtimeType info from storage"); + } else { + Logger().i( + "Session is not persistent; skipping $runtimeType load from storage"); } } - Future _loadFromRemote(Session session, Profile profile) async { + Future _loadFromRemote(Session session, Profile profile, + {bool force = false}) async { final bool hasConnectivity = await Connectivity().checkConnectivity() != ConnectivityResult.none; - if (hasConnectivity) { - updateStatus(RequestStatus.busy); - await loadFromRemote(session, profile); + final shouldReload = force || + _lastUpdateTime == null || + cacheDuration == null || + DateTime.now().difference(_lastUpdateTime!) > cacheDuration!; + + if (shouldReload) { + if (hasConnectivity) { + updateStatus(RequestStatus.busy); + await loadFromRemote(session, profile); + if (_status == RequestStatus.successful) { + Logger().i("Loaded $runtimeType info from remote"); + } else if (_status == RequestStatus.failed) { + Logger().e("Failed to load $runtimeType info from remote"); + } + } else { + Logger().i("No internet connection; skipping $runtimeType remote load"); + } + } else { + Logger().i( + "Last info for $runtimeType is within cache period ($cacheDuration); skipping remote load"); } - if (!hasConnectivity || _status == RequestStatus.busy) { + if (!shouldReload || !hasConnectivity || _status == RequestStatus.busy) { // No online activity from provider updateStatus(RequestStatus.successful); } else { @@ -65,7 +94,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { final profile = Provider.of(context, listen: false).profile; - _loadFromRemote(session, profile); + _loadFromRemote(session, profile, force: true); } Future ensureInitialized(Session session, Profile profile) async { From c41385e2da31cc8b2cdda06320bd47e2ed88844e Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 12 Jul 2023 21:02:30 +0100 Subject: [PATCH 045/100] Make generic expansion card stateless --- .../common_widgets/generic_expansion_card.dart | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index 072737f91..68f106ffb 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -1,25 +1,19 @@ -import 'package:flutter/material.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; +import 'package:flutter/material.dart'; /// Card with a expansible child -abstract class GenericExpansionCard extends StatefulWidget { +abstract class GenericExpansionCard extends StatelessWidget { const GenericExpansionCard({Key? key}) : super(key: key); - @override - State createState() { - return GenericExpansionCardState(); - } - TextStyle? getTitleStyle(BuildContext context) => Theme.of(context) .textTheme .headlineSmall ?.apply(color: Theme.of(context).primaryColor); String getTitle(); + Widget buildCardContent(BuildContext context); -} -class GenericExpansionCardState extends State { @override Widget build(BuildContext context) { return Container( @@ -31,12 +25,12 @@ class GenericExpansionCardState extends State { expandedColor: (Theme.of(context).brightness == Brightness.light) ? const Color.fromARGB(0xf, 0, 0, 0) : const Color.fromARGB(255, 43, 43, 43), - title: Text(widget.getTitle(), style: widget.getTitleStyle(context)), + title: Text(getTitle(), style: getTitleStyle(context)), elevation: 0, children: [ Container( padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), - child: widget.buildCardContent(context), + child: buildCardContent(context), ) ], )); From 8142b3fb60f06eca957b02d99f3b2f7c5d36f638 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 12 Jul 2023 21:38:12 +0100 Subject: [PATCH 046/100] Encapsulate card refresh logic --- .../providers/faculty_locations_provider.dart | 1 - uni/lib/view/common_widgets/generic_card.dart | 4 +- .../widgets/course_unit_card.dart | 3 ++ uni/lib/view/home/home.dart | 52 +++---------------- uni/lib/view/home/widgets/bus_stop_card.dart | 6 +++ uni/lib/view/home/widgets/exam_card.dart | 6 +++ .../view/home/widgets/main_cards_list.dart | 4 +- .../view/home/widgets/restaurant_card.dart | 9 +++- uni/lib/view/home/widgets/schedule_card.dart | 6 +++ .../widgets/library_occupation_card.dart | 7 +++ .../profile/widgets/account_info_card.dart | 8 +++ .../profile/widgets/course_info_card.dart | 50 +++++++++++++----- .../view/profile/widgets/print_info_card.dart | 6 +++ .../widgets/restaurant_page_card.dart | 5 +- 14 files changed, 103 insertions(+), 64 deletions(-) delete mode 100644 uni/lib/model/providers/faculty_locations_provider.dart diff --git a/uni/lib/model/providers/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart deleted file mode 100644 index 613744d49..000000000 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement this library. diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index dc92d9d04..03823f2f3 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -32,7 +32,9 @@ abstract class GenericCard extends StatefulWidget { String getTitle(); - dynamic onClick(BuildContext context); + void onClick(BuildContext context); + + void onRefresh(BuildContext context); Text getInfoText(String text, BuildContext context) { return Text(text, diff --git a/uni/lib/view/course_units/widgets/course_unit_card.dart b/uni/lib/view/course_units/widgets/course_unit_card.dart index a3df1298d..2b424fdac 100644 --- a/uni/lib/view/course_units/widgets/course_unit_card.dart +++ b/uni/lib/view/course_units/widgets/course_unit_card.dart @@ -42,4 +42,7 @@ class CourseUnitCard extends GenericCard { MaterialPageRoute( builder: (context) => CourseUnitDetailPageView(courseUnit))); } + + @override + void onRefresh(BuildContext context) {} } diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index 859c44d41..a1c99a25c 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,14 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; -import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/model/providers/lazy/home_page_provider.dart'; -import 'package:uni/model/providers/lazy/lecture_provider.dart'; -import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; -import 'package:uni/model/providers/lazy/reference_provider.dart'; -import 'package:uni/model/providers/startup/profile_provider.dart'; -import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; @@ -23,47 +15,19 @@ class HomePageView extends StatefulWidget { class HomePageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return MainCardsList(); + return const MainCardsList(); } @override Future handleRefresh(BuildContext context) async { - final homePageProvider = - Provider.of(context, listen: false); + final favoriteCardTypes = context.read().favoriteCards; + final cards = favoriteCardTypes + .map((e) => + MainCardsList.cardCreators[e]!(const Key(""), false, () => null)) + .toList(); - final providersToUpdate = {}; - - for (final cardType in homePageProvider.favoriteCards) { - switch (cardType) { - case FavoriteWidgetType.account: - providersToUpdate - .add(Provider.of(context, listen: false)); - providersToUpdate - .add(Provider.of(context, listen: false)); - break; - case FavoriteWidgetType.exams: - providersToUpdate - .add(Provider.of(context, listen: false)); - break; - case FavoriteWidgetType.schedule: - providersToUpdate - .add(Provider.of(context, listen: false)); - break; - case FavoriteWidgetType.printBalance: - providersToUpdate - .add(Provider.of(context, listen: false)); - break; - case FavoriteWidgetType.libraryOccupation: - providersToUpdate.add( - Provider.of(context, listen: false)); - break; - case FavoriteWidgetType.busStops: - providersToUpdate - .add(Provider.of(context, listen: false)); - break; - } + for (final card in cards) { + card.onRefresh(context); } - - Future.wait(providersToUpdate.map((e) => e.forceRefresh(context))); } } diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 0f981093d..0e66d9cae 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; @@ -31,6 +32,11 @@ class BusStopCard extends GenericCard { }, ); } + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } } /// Returns a widget with the bus stop card final content diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 92c10ba38..b7b7f4fe9 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/utils/drawer_items.dart'; @@ -26,6 +27,11 @@ class ExamCard extends GenericCard { onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navExams.title}'); + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } + /// Returns a widget with all the exams card content. /// /// If there are no exams, a message telling the user diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index bf4156ae9..7871c81d7 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -19,7 +19,7 @@ typedef CardCreator = GenericCard Function( Key key, bool isEditingMode, dynamic Function()? onDelete); class MainCardsList extends StatelessWidget { - final Map cardCreators = { + static Map cardCreators = { FavoriteWidgetType.schedule: (k, em, od) => ScheduleCard.fromEditingInformation(k, em, od), FavoriteWidgetType.exams: (k, em, od) => @@ -37,7 +37,7 @@ class MainCardsList extends StatelessWidget { LibraryOccupationCard.fromEditingInformation(k, em, od) }; - MainCardsList({super.key}); + const MainCardsList({super.key}); @override Widget build(BuildContext context) { diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 39c440043..8efb549e8 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; @@ -18,7 +19,13 @@ class RestaurantCard extends GenericCard { String getTitle() => 'Cantinas'; @override - onClick(BuildContext context) => null; + onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false) + .forceRefresh(context); + } @override Widget buildCardContent(BuildContext context) { diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index d0f066ec8..a525b8e96 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; @@ -21,6 +22,11 @@ class ScheduleCard extends GenericCard { final double leftPadding = 12.0; final List lectures = []; + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } + @override Widget buildCardContent(BuildContext context) { return LazyConsumer( diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index b4af4b85f..d4265d2c5 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/percent_indicator.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; @@ -22,6 +23,12 @@ class LibraryOccupationCard extends GenericCard { onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navLibrary.title}'); + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false) + .forceRefresh(context); + } + @override Widget buildCardContent(BuildContext context) { return LazyConsumer( diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index ad4417de3..14e2bce92 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/entities/reference.dart'; import 'package:uni/model/providers/lazy/reference_provider.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; @@ -16,6 +17,13 @@ class AccountInfoCard extends GenericCard { Key key, bool editingMode, Function()? onDelete) : super.fromEditingInformation(key, editingMode, onDelete); + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + Provider.of(context, listen: false) + .forceRefresh(context); + } + @override Widget buildCardContent(BuildContext context) { return LazyConsumer( diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index 86fb0b052..f328fbc25 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -18,11 +18,14 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 20.0), child: Text('Ano curricular atual: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 20.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 20.0, bottom: 8.0, right: 20.0), child: getInfoText(course.currYear ?? 'Indisponível', context), ) ]), @@ -30,11 +33,14 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Estado atual: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText(course.state ?? 'Indisponível', context), ) ]), @@ -42,14 +48,18 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Ano da primeira inscrição: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.firstEnrollment != null - ? '${course.firstEnrollment}/${course.firstEnrollment! + 1}' + ? '${course.firstEnrollment}/${course.firstEnrollment! + + 1}' : '?', context)) ]), @@ -57,11 +67,14 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Faculdade: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.faculty?.toUpperCase() ?? 'Indisponível', context)) ]), @@ -69,11 +82,14 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Média: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.currentAverage?.toString() ?? 'Indisponível', context)) @@ -81,13 +97,16 @@ class CourseInfoCard extends GenericCard { TableRow(children: [ Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), child: Text('ECTs realizados: ', - style: Theme.of(context).textTheme.titleSmall), + style: Theme + .of(context) + .textTheme + .titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 20.0, right: 20.0), child: getInfoText( course.finishedEcts?.toString().replaceFirst('.0', '') ?? '?', @@ -103,4 +122,7 @@ class CourseInfoCard extends GenericCard { @override onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) {} } diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index bb41169b5..d53c38bbb 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -63,4 +64,9 @@ class PrintInfoCard extends GenericCard { @override onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } } diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index 062fe8a88..c615fa757 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -7,7 +7,7 @@ class RestaurantPageCard extends GenericCard { RestaurantPageCard(this.restaurantName, this.meals, {super.key}) : super.customStyle( - editingMode: false, onDelete: () => null, smallTitle: true); + editingMode: false, onDelete: () => null, smallTitle: true); @override Widget buildCardContent(BuildContext context) { @@ -21,4 +21,7 @@ class RestaurantPageCard extends GenericCard { @override onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) {} } From 4dde44f40d748de1e75c60aed4601f6ba18cfe27 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 12 Jul 2023 21:48:55 +0100 Subject: [PATCH 047/100] Do not allow concurrent refreshes --- .../providers/state_provider_notifier.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index fb56a4d51..22f81de89 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -68,7 +68,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { "$runtimeType remote load method did not update request status"); } } else { - Logger().i("No internet connection; skipping $runtimeType remote load"); + Logger().w("No internet connection; skipping $runtimeType remote load"); } } else { Logger().i( @@ -92,12 +92,22 @@ abstract class StateProviderNotifier extends ChangeNotifier { } Future forceRefresh(BuildContext context) async { - final session = - Provider.of(context, listen: false).session; - final profile = - Provider.of(context, listen: false).profile; + await _lock.synchronized(() async { + if (_lastUpdateTime != null && + DateTime.now().difference(_lastUpdateTime!) < + const Duration(minutes: 1)) { + Logger().w( + "Last update for $runtimeType was less than a minute ago; skipping refresh"); + return; + } - _loadFromRemote(session, profile, force: true); + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; + + _loadFromRemote(session, profile, force: true); + }); } Future ensureInitialized(Session session, Profile profile) async { From 267c1fe99d8e8a2f796a72d371cedf5d500e4560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Duarte?= Date: Mon, 10 Jul 2023 17:32:26 +0100 Subject: [PATCH 048/100] Simple fix for when there are no more tuitions --- .../background_workers/notifications/tuition_notification.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 777084d75..176f18e0e 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -47,6 +47,9 @@ class TuitionNotification extends Notification { final FeesFetcher feesFetcher = FeesFetcher(); final String nextDueDate = await parseFeesNextLimit( await feesFetcher.getUserFeesResponse(session)); + if(nextDueDate == "Sem data"){ + return false; + } _dueDate = DateTime.parse(nextDueDate); return DateTime.now().difference(_dueDate).inDays >= -3; } From 6e0f6baa9e4c51c516d8aa28d9467a5a57420cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Duarte?= Date: Mon, 10 Jul 2023 18:18:51 +0100 Subject: [PATCH 049/100] Small refactor to make parser return datetime instead of a string --- .../notifications/tuition_notification.dart | 10 +-- .../local_storage/app_user_database.dart | 20 +++--- uni/lib/controller/parsers/parser_fees.dart | 8 ++- uni/lib/model/entities/profile.dart | 4 +- .../providers/profile_state_provider.dart | 4 +- .../profile/widgets/account_info_card.dart | 65 +++++++++---------- 6 files changed, 55 insertions(+), 56 deletions(-) diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 176f18e0e..9b8c3f5a9 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -45,12 +45,12 @@ class TuitionNotification extends Notification { !(await AppSharedPreferences.getTuitionNotificationToggle()); if (notificationsAreDisabled) return false; final FeesFetcher feesFetcher = FeesFetcher(); - final String nextDueDate = await parseFeesNextLimit( + final DateTime? dueDate = await parseFeesNextLimit( await feesFetcher.getUserFeesResponse(session)); - if(nextDueDate == "Sem data"){ - return false; - } - _dueDate = DateTime.parse(nextDueDate); + + if(dueDate == null) return false; + + _dueDate = dueDate; return DateTime.now().difference(_dueDate).inDays >= -3; } diff --git a/uni/lib/controller/local_storage/app_user_database.dart b/uni/lib/controller/local_storage/app_user_database.dart index a8c728bc6..623589104 100644 --- a/uni/lib/controller/local_storage/app_user_database.dart +++ b/uni/lib/controller/local_storage/app_user_database.dart @@ -31,13 +31,14 @@ class AppUserDataDatabase extends AppDatabase { final List> maps = await db.query('userdata'); // Convert the List into a Profile. - String? name, email, printBalance, feesBalance, feesLimit; + String? name, email, printBalance, feesBalance; + DateTime? feesLimit; for (Map entry in maps) { if (entry['key'] == 'name') name = entry['value']; if (entry['key'] == 'email') email = entry['value']; if (entry['key'] == 'printBalance') printBalance = entry['value']; if (entry['key'] == 'feesBalance') feesBalance = entry['value']; - if (entry['key'] == 'feesLimit') feesLimit = entry['value']; + if (entry['key'] == 'feesLimit') feesLimit = DateTime.tryParse(entry['value']); } return Profile( @@ -46,7 +47,7 @@ class AppUserDataDatabase extends AppDatabase { courses: [], printBalance: printBalance ?? '?', feesBalance: feesBalance ?? '?', - feesLimit: feesLimit ?? '?'); + feesLimit: feesLimit); } /// Deletes all of the data stored in this database. @@ -65,13 +66,12 @@ class AppUserDataDatabase extends AppDatabase { /// Saves the user's balance and payment due date to the database. /// - /// *Note:* - /// * the first value in [feesInfo] is the user's balance. - /// * the second value in [feesInfo] is the user's payment due date. - void saveUserFees(Tuple2 feesInfo) async { + void saveUserFees(String feesBalance, DateTime? feesLimit) async { await insertInDatabase( - 'userdata', {'key': 'feesBalance', 'value': feesInfo.item1}); - await insertInDatabase( - 'userdata', {'key': 'feesLimit', 'value': feesInfo.item2}); + 'userdata', {'key': 'feesBalance', 'value': feesBalance}); + await insertInDatabase('userdata', { + 'key': 'feesLimit', + 'value': feesLimit != null ? feesLimit.toIso8601String() : '' + }); } } diff --git a/uni/lib/controller/parsers/parser_fees.dart b/uni/lib/controller/parsers/parser_fees.dart index cd1837dcc..20c7a4a55 100644 --- a/uni/lib/controller/parsers/parser_fees.dart +++ b/uni/lib/controller/parsers/parser_fees.dart @@ -17,15 +17,17 @@ Future parseFeesBalance(http.Response response) async { /// Extracts the user's payment due date from an HTTP [response]. /// /// If there are no due payments, `Sem data` is returned. -Future parseFeesNextLimit(http.Response response) async { +Future parseFeesNextLimit(http.Response response) async { final document = parse(response.body); final lines = document.querySelectorAll('#tab0 .tabela tr'); if (lines.length < 2) { - return 'Sem data'; + return null; } final String limit = lines[1].querySelectorAll('.data')[1].text; - return limit; + //it's completly fine to throw an exeception if it fails, in this case, + //since probably sigarra is returning something we don't except + return DateTime.parse(limit); } diff --git a/uni/lib/model/entities/profile.dart b/uni/lib/model/entities/profile.dart index 6c222c657..3af0e8b85 100644 --- a/uni/lib/model/entities/profile.dart +++ b/uni/lib/model/entities/profile.dart @@ -10,7 +10,7 @@ class Profile { late List courses; final String printBalance; final String feesBalance; - final String feesLimit; + final DateTime? feesLimit; Profile( {this.name = '', @@ -18,7 +18,7 @@ class Profile { courses, this.printBalance = '', this.feesBalance = '', - this.feesLimit = ''}) + this.feesLimit}) : courses = courses ?? []; /// Creates a new instance from a JSON object. diff --git a/uni/lib/model/providers/profile_state_provider.dart b/uni/lib/model/providers/profile_state_provider.dart index 619b7a6de..efc00b4f4 100644 --- a/uni/lib/model/providers/profile_state_provider.dart +++ b/uni/lib/model/providers/profile_state_provider.dart @@ -63,7 +63,7 @@ class ProfileStateProvider extends StateProviderNotifier { final response = await FeesFetcher().getUserFeesResponse(session); final String feesBalance = await parseFeesBalance(response); - final String feesLimit = await parseFeesNextLimit(response); + final DateTime? feesLimit = await parseFeesNextLimit(response); final DateTime currentTime = DateTime.now(); final Tuple2 userPersistentInfo = @@ -73,7 +73,7 @@ class ProfileStateProvider extends StateProviderNotifier { // Store fees info final profileDb = AppUserDataDatabase(); - profileDb.saveUserFees(Tuple2(feesBalance, feesLimit)); + profileDb.saveUserFees(feesBalance, feesLimit); } final Profile newProfile = Profile( diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 627642116..036de20a6 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/reference.dart'; import 'package:uni/model/providers/profile_state_provider.dart'; @@ -50,37 +51,34 @@ class AccountInfoCard extends GenericCard { Container( margin: const EdgeInsets.only( top: 8.0, bottom: 20.0, right: 30.0), - child: getInfoText(profile.feesLimit, context)) + child: getInfoText( + profile.feesLimit != null + ? DateFormat('yyyy-MM-dd') + .format(profile.feesLimit!) + : 'Sem data', + context)) ]), TableRow(children: [ Container( - margin: - const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), - child: Text("Notificar próxima data limite: ", - style: Theme.of(context).textTheme.titleSmall) - ), + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: Text("Notificar próxima data limite: ", + style: Theme.of(context).textTheme.titleSmall)), Container( - margin: - const EdgeInsets.only(top: 8.0, bottom: 20.0, left: 20.0), - child: - const TuitionNotificationSwitch() - ) + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: const TuitionNotificationSwitch()) ]) ]), Container( padding: const EdgeInsets.all(10), - child: Row( - children: [ - Text('Referências pendentes', - style: Theme.of(context).textTheme.titleLarge - ?.apply(color: Theme.of(context).colorScheme.secondary)), - ] - ) - ), + child: Row(children: [ + Text('Referências pendentes', + style: Theme.of(context).textTheme.titleLarge?.apply( + color: Theme.of(context).colorScheme.secondary)), + ])), ReferenceWidgets(references: references), - const SizedBox( - height: 10 - ), + const SizedBox(height: 10), showLastRefreshedTime(profileStateProvider.feesRefreshTime, context) ]); }, @@ -97,7 +95,8 @@ class AccountInfoCard extends GenericCard { class ReferenceWidgets extends StatelessWidget { final List references; - const ReferenceWidgets({Key? key, required this.references}): super(key: key); + const ReferenceWidgets({Key? key, required this.references}) + : super(key: key); @override Widget build(BuildContext context) { @@ -114,16 +113,14 @@ class ReferenceWidgets extends StatelessWidget { if (references.length == 1) { return ReferenceSection(reference: references[0]); } - return Column( - children: [ - ReferenceSection(reference: references[0]), - const Divider( - thickness: 1, - indent: 30, - endIndent: 30, - ), - ReferenceSection(reference: references[1]), - ] - ); + return Column(children: [ + ReferenceSection(reference: references[0]), + const Divider( + thickness: 1, + indent: 30, + endIndent: 30, + ), + ReferenceSection(reference: references[1]), + ]); } } From 525be3c0517a474aeb675377b48d1de8fe014e68 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 12 Jul 2023 23:33:27 +0100 Subject: [PATCH 050/100] Fix compilation with flutter_widget_from_html --- uni/android/app/build.gradle | 2 +- uni/android/build.gradle | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/uni/android/app/build.gradle b/uni/android/app/build.gradle index 018363437..1e13ce5bc 100644 --- a/uni/android/app/build.gradle +++ b/uni/android/app/build.gradle @@ -52,7 +52,7 @@ android { applicationId "pt.up.fe.ni.uni" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 19 // default is flutter.minSdkVersion + minSdkVersion 21 // default is flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/uni/android/build.gradle b/uni/android/build.gradle index 96de58432..69e24e2f4 100644 --- a/uni/android/build.gradle +++ b/uni/android/build.gradle @@ -1,12 +1,14 @@ buildscript { ext.kotlin_version = '1.7.21' + ext.android_plugin_version = '7.2.0' + repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath "com.android.tools.build:gradle:$android_plugin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 0525ff942aa0e13831da10db36c24bd5924a5d31 Mon Sep 17 00:00:00 2001 From: bdmendes Date: Wed, 12 Jul 2023 22:35:40 +0000 Subject: [PATCH 051/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 87a049733..7f0f07fca 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.31+149 \ No newline at end of file +1.5.32+150 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 79103313c..6fee1b030 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.31+149 +version: 1.5.32+150 environment: sdk: ">=2.17.1 <3.0.0" From 25f114c03bec5176e0236e43a64b97f3fb6e736d Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 12 Jul 2023 23:59:11 +0100 Subject: [PATCH 052/100] Fix parsing of empty classes --- .../parsers/parser_course_unit_info.dart | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 0715d15a6..78005b951 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -19,8 +19,8 @@ Future parseCourseUnitSheet(http.Response response) async { return CourseUnitSheet(sections); } -List parseCourseUnitClasses( - http.Response response, String baseUrl) { +List parseCourseUnitClasses(http.Response response, + String baseUrl) { final List classes = []; final document = parse(response.body); final titles = document.querySelectorAll('#conteudoinner h3').sublist(1); @@ -30,25 +30,29 @@ List parseCourseUnitClasses( final String className = title.innerHtml.substring( title.innerHtml.indexOf(' ') + 1, title.innerHtml.indexOf('&')); - final studentRows = table?.querySelectorAll('tr').sublist(1); + final rows = table?.querySelectorAll('tr'); + if (rows == null || rows.length < 2) { + continue; + } + + final studentRows = rows.sublist(1); final List students = []; - if (studentRows != null) { - for (final row in studentRows) { - final columns = row.querySelectorAll('td.k.t'); - final String studentName = columns[0].children[0].innerHtml; - final int studentNumber = - int.tryParse(columns[1].innerHtml.trim()) ?? 0; - final String studentMail = columns[2].innerHtml; - - final Uri studentPhoto = Uri.parse( - "${baseUrl}fotografias_service.foto?pct_cod=$studentNumber"); - final Uri studentProfile = Uri.parse( - "${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber"); - students.add(CourseUnitStudent(studentName, studentNumber, studentMail, - studentPhoto, studentProfile)); - } + for (final row in studentRows) { + final columns = row.querySelectorAll('td.k.t'); + final String studentName = columns[0].children[0].innerHtml; + final int studentNumber = + int.tryParse(columns[1].innerHtml.trim()) ?? 0; + final String studentMail = columns[2].innerHtml; + + final Uri studentPhoto = Uri.parse( + "${baseUrl}fotografias_service.foto?pct_cod=$studentNumber"); + final Uri studentProfile = Uri.parse( + "${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber"); + students.add(CourseUnitStudent(studentName, studentNumber, studentMail, + studentPhoto, studentProfile)); } + classes.add(CourseUnitClass(className, students)); } From 884e3b6fe6a0a0e7f7842f13cb0bb2ece3676dfe Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Thu, 13 Jul 2023 15:00:25 +0100 Subject: [PATCH 053/100] Always load from storage --- .../model/providers/state_provider_notifier.dart | 14 ++------------ uni/lib/view/navigation_service.dart | 3 ++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 22f81de89..a95232d8f 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -32,18 +32,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( runtimeType.toString()); - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - final sessionIsPersistent = - userPersistentInfo.item1 != '' && userPersistentInfo.item2 != ''; - - if (sessionIsPersistent) { - await loadFromStorage(); - Logger().i("Loaded $runtimeType info from storage"); - } else { - Logger().i( - "Session is not persistent; skipping $runtimeType load from storage"); - } + await loadFromStorage(); + Logger().i("Loaded $runtimeType info from storage"); } Future _loadFromRemote(Session session, Profile profile, diff --git a/uni/lib/view/navigation_service.dart b/uni/lib/view/navigation_service.dart index 89bf885b2..233faccf9 100644 --- a/uni/lib/view/navigation_service.dart +++ b/uni/lib/view/navigation_service.dart @@ -5,8 +5,9 @@ import 'package:uni/utils/drawer_items.dart'; class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); + static logout() { - navigatorKey.currentState!.pushNamedAndRemoveUntil( + navigatorKey.currentState?.pushNamedAndRemoveUntil( '/${DrawerItem.navLogOut.title}', (_) => false); } } From 488b4f97f7ded6bf43122c5cdf62e0523982b34f Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 14:22:06 +0100 Subject: [PATCH 054/100] Tweak request dependant widget on request failed edge cases --- .../request_dependent_widget_builder.dart | 18 +++--- .../course_unit_info/course_unit_info.dart | 62 +++++++++++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index 29502ddf1..c1e893c2f 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -55,14 +55,18 @@ class RequestDependentWidgetBuilder extends StatelessWidget { return FutureBuilder( future: Connectivity().checkConnectivity(), builder: (BuildContext context, AsyncSnapshot connectivitySnapshot) { - if (connectivitySnapshot.hasData) { - if (connectivitySnapshot.data == ConnectivityResult.none) { - return Center( - heightFactor: 3, - child: Text('Sem ligação à internet', - style: Theme.of(context).textTheme.titleMedium)); - } + if (!connectivitySnapshot.hasData) { + return const Center( + heightFactor: 3, child: CircularProgressIndicator()); } + + if (connectivitySnapshot.data == ConnectivityResult.none) { + return Center( + heightFactor: 3, + child: Text('Sem ligação à internet', + style: Theme.of(context).textTheme.titleMedium)); + } + return Column(children: [ Padding( padding: const EdgeInsets.only(top: 15, bottom: 10), 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 a9c4a3705..b6d25cee5 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -25,25 +25,36 @@ class CourseUnitDetailPageView extends StatefulWidget { class CourseUnitDetailPageViewState extends SecondaryPageViewState { - @override - Future onLoad(BuildContext context) async { + Future loadInfo(bool force) async { final courseUnitsProvider = - Provider.of(context, listen: false); - final session = context.read().session; + Provider.of(context, listen: false); + final session = context + .read() + .session; final CourseUnitSheet? courseUnitSheet = - courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; - if (courseUnitSheet == null) { + courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; + if (courseUnitSheet == null || force) { courseUnitsProvider.getCourseUnitSheet(widget.courseUnit, session); } final List? courseUnitClasses = - courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; - if (courseUnitClasses == null) { + courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; + if (courseUnitClasses == null || force) { courseUnitsProvider.getCourseUnitClasses(widget.courseUnit, session); } } + @override + Future onRefresh(BuildContext context) async { + loadInfo(true); + } + + @override + Future onLoad(BuildContext context) async { + loadInfo(false); + } + @override Widget getBody(BuildContext context) { return DefaultTabController( @@ -73,31 +84,32 @@ class CourseUnitDetailPageViewState Widget _courseUnitSheetView(BuildContext context) { return LazyConsumer( builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), - status: courseUnitsInfoProvider.status, - builder: () => CourseUnitSheetView( - courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]!), - hasContentPredicate: + return RequestDependentWidgetBuilder( + onNullContent: const Center(), + status: courseUnitsInfoProvider.status, + builder: () => + CourseUnitSheetView( + courseUnitsInfoProvider.courseUnitsSheets[widget + .courseUnit]!), + hasContentPredicate: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != null); - }); + }); } Widget _courseUnitClassesView(BuildContext context) { return LazyConsumer( builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), - status: courseUnitsInfoProvider.status, - builder: () => CourseUnitClassesView( - courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit]!), - hasContentPredicate: + return RequestDependentWidgetBuilder( + onNullContent: const Center(), + status: courseUnitsInfoProvider.status, + builder: () => + CourseUnitClassesView( + courseUnitsInfoProvider.courseUnitsClasses[widget + .courseUnit]!), + hasContentPredicate: courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit] != null); - }); + }); } - - @override - Future onRefresh(BuildContext context) async {} } From f9fddc5bcadd959a7c5f6a581904cca64694e3c4 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 14:44:13 +0100 Subject: [PATCH 055/100] Fix students profile picture --- uni/lib/model/providers/startup/profile_provider.dart | 7 ++++--- .../course_unit_info/widgets/course_unit_student_row.dart | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index bf905e446..ca37ec3e0 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -234,16 +234,17 @@ class ProfileProvider extends StateProviderNotifier { } static Future fetchOrGetCachedProfilePicture( - String? studentNumber, Session session, + int? studentNumber, Session session, {forceRetrieval = false}) { - studentNumber ??= session.studentNumber; + studentNumber ??= int.parse(session.studentNumber.replaceAll("up", "")); + final String faculty = session.faculties[0]; final String url = 'https://sigarra.up.pt/$faculty/pt/fotografias_service.foto?pct_cod=$studentNumber'; final Map headers = {}; headers['cookie'] = session.cookies; return loadFileFromStorageOrRetrieveNew( - 'user_profile_picture', url, headers, + '${studentNumber}_profile_picture', url, headers, forceRetrieval: forceRetrieval); } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart index 5093df2d3..860ed1791 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart @@ -15,8 +15,7 @@ class CourseUnitStudentRow extends StatelessWidget { @override Widget build(BuildContext context) { final Future userImage = - ProfileProvider.fetchOrGetCachedProfilePicture( - "up${student.number}", session); + ProfileProvider.fetchOrGetCachedProfilePicture(student.number, session); return FutureBuilder( builder: (BuildContext context, AsyncSnapshot snapshot) { return Container( From e040fe304b99bcf5ef725ac0750b6d5f7f41ee36 Mon Sep 17 00:00:00 2001 From: Sirze01 Date: Fri, 14 Jul 2023 13:45:37 +0000 Subject: [PATCH 056/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 7f0f07fca..ab41fa991 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.32+150 \ No newline at end of file +1.5.33+151 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 6fee1b030..d5b94b4a1 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.32+150 +version: 1.5.33+151 environment: sdk: ">=2.17.1 <3.0.0" From e2e0f8deb3b239ddc9b9b435ccb4653bf2d9616e Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 14:51:55 +0100 Subject: [PATCH 057/100] Remove students link --- .../widgets/course_unit_student_row.dart | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart index 860ed1791..74b17ae3a 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; class CourseUnitStudentRow extends StatelessWidget { const CourseUnitStudentRow(this.student, this.session, {super.key}); @@ -35,24 +34,20 @@ class CourseUnitStudentRow extends StatelessWidget { : const AssetImage( 'assets/images/profile_placeholder.png')))), Expanded( - child: InkWell( - onTap: () => launchUrl(student.profile), - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(student.name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyLarge), - Opacity( - opacity: 0.8, - child: Text( - "up${student.number}", - )) - ])))) + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(student.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge), + Opacity( + opacity: 0.8, + child: Text( + "up${student.number}", + )) + ]))) ], )); }, From dedac874584199f2a76efc9489b1cf8f16cb7023 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 15:11:45 +0100 Subject: [PATCH 058/100] Save images on the cache instead --- uni/lib/controller/local_storage/file_offline_storage.dart | 2 +- uni/pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/uni/lib/controller/local_storage/file_offline_storage.dart b/uni/lib/controller/local_storage/file_offline_storage.dart index 9b6dc178b..cb24fafc6 100644 --- a/uni/lib/controller/local_storage/file_offline_storage.dart +++ b/uni/lib/controller/local_storage/file_offline_storage.dart @@ -8,7 +8,7 @@ import 'package:uni/controller/networking/network_router.dart'; /// The offline image storage location on the device. Future get _localPath async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getTemporaryDirectory(); return directory.path; } diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 62b9bdb25..eecfe0078 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -44,7 +44,6 @@ dependencies: path_provider: ^2.0.0 sqflite: ^2.0.3 path: ^1.8.0 - cached_network_image: ^3.0.0-nullsafety flutter_svg: ^2.0.0+1 synchronized: ^3.0.0 image: ^4.0.13 From 70a0d7c284a6da6c42dd6eef400718c4504c91c3 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 15:40:05 +0100 Subject: [PATCH 059/100] Do not trigger course units info provider initialization --- .../lazy/course_units_info_provider.dart | 6 +-- .../providers/state_provider_notifier.dart | 8 ++-- .../course_unit_info/course_unit_info.dart | 46 ++++++++----------- 3 files changed, 28 insertions(+), 32 deletions(-) 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 5bafea81f..8b3851264 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -15,7 +15,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { final Map> _courseUnitsClasses = {}; CourseUnitsInfoProvider() - : super(dependsOnSession: true, cacheDuration: null); + : super(dependsOnSession: true, cacheDuration: null, initialize: false); UnmodifiableMapView get courseUnitsSheets => UnmodifiableMapView(_courseUnitsSheets); @@ -23,7 +23,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { UnmodifiableMapView> get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); - getCourseUnitSheet(CourseUnit courseUnit, Session session) async { + fetchCourseUnitSheet(CourseUnit courseUnit, Session session) async { updateStatus(RequestStatus.busy); try { _courseUnitsSheets[courseUnit] = await CourseUnitsInfoFetcher() @@ -36,7 +36,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } - getCourseUnitClasses(CourseUnit courseUnit, Session session) async { + fetchCourseUnitClasses(CourseUnit courseUnit, Session session) async { updateStatus(RequestStatus.busy); try { _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index a95232d8f..c7fbe29a5 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -13,7 +13,7 @@ import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { static final Lock _lock = Lock(); RequestStatus _status; - bool _initialized = false; + bool _initialized; DateTime? _lastUpdateTime; bool dependsOnSession; Duration? cacheDuration; @@ -25,8 +25,10 @@ abstract class StateProviderNotifier extends ChangeNotifier { StateProviderNotifier( {required this.dependsOnSession, required this.cacheDuration, - RequestStatus? initialStatus}) - : _status = initialStatus ?? RequestStatus.busy; + RequestStatus initialStatus = RequestStatus.busy, + bool initialize = true}) + : _status = initialStatus, + _initialized = !initialize; Future _loadFromStorage() async { _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( 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 b6d25cee5..b3f40b3e0 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -27,21 +27,19 @@ class CourseUnitDetailPageViewState extends SecondaryPageViewState { Future loadInfo(bool force) async { final courseUnitsProvider = - Provider.of(context, listen: false); - final session = context - .read() - .session; + Provider.of(context, listen: false); + final session = context.read().session; final CourseUnitSheet? courseUnitSheet = - courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; + courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; if (courseUnitSheet == null || force) { - courseUnitsProvider.getCourseUnitSheet(widget.courseUnit, session); + courseUnitsProvider.fetchCourseUnitSheet(widget.courseUnit, session); } final List? courseUnitClasses = - courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; + courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; if (courseUnitClasses == null || force) { - courseUnitsProvider.getCourseUnitClasses(widget.courseUnit, session); + courseUnitsProvider.fetchCourseUnitClasses(widget.courseUnit, session); } } @@ -84,32 +82,28 @@ class CourseUnitDetailPageViewState Widget _courseUnitSheetView(BuildContext context) { return LazyConsumer( builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), - status: courseUnitsInfoProvider.status, - builder: () => - CourseUnitSheetView( - courseUnitsInfoProvider.courseUnitsSheets[widget - .courseUnit]!), - hasContentPredicate: + return RequestDependentWidgetBuilder( + onNullContent: const Center(), + status: courseUnitsInfoProvider.status, + builder: () => CourseUnitSheetView( + courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]!), + hasContentPredicate: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != null); - }); + }); } Widget _courseUnitClassesView(BuildContext context) { return LazyConsumer( builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), - status: courseUnitsInfoProvider.status, - builder: () => - CourseUnitClassesView( - courseUnitsInfoProvider.courseUnitsClasses[widget - .courseUnit]!), - hasContentPredicate: + return RequestDependentWidgetBuilder( + onNullContent: const Center(), + status: courseUnitsInfoProvider.status, + builder: () => CourseUnitClassesView( + courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit]!), + hasContentPredicate: courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit] != null); - }); + }); } } From 04526ea4a66e58690abb407bba5f51ef77d017ef Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 15:49:15 +0100 Subject: [PATCH 060/100] Bring back cached network image --- uni/lib/view/course_units/course_units.dart | 2 +- uni/pubspec.yaml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 84e3564b9..790a4e894 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -78,7 +78,7 @@ class CourseUnitsPageViewState return Column(children: [ _getPageTitleAndFilters(availableYears, availableSemesters), RequestDependentWidgetBuilder( - status: requestStatus ?? RequestStatus.none, + status: requestStatus, builder: () => _generateCourseUnitsCards(filteredCourseUnits, context), hasContentPredicate: courseUnits?.isNotEmpty ?? false, diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index eecfe0078..0334ddcd8 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -61,8 +61,7 @@ dependencies: collection: ^1.16.0 timelines: ^0.1.0 flutter_map: ^4.0.0 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + cached_network_image: ^3.2.3 cupertino_icons: ^1.0.2 latlong2: ^0.8.1 flutter_map_marker_popup: ^4.0.1 From ca65a669ac87bed9b96674456816b040dcb30ec5 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 14 Jul 2023 16:08:24 +0100 Subject: [PATCH 061/100] Replace html package by its smaller version --- uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index cd3505e34..133100a7d 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; import 'package:html/dom.dart' as dom; import 'package:provider/provider.dart'; import 'package:uni/controller/networking/network_router.dart'; diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 0334ddcd8..9fac03c6f 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: workmanager: ^0.5.1 flutter_local_notifications: ^15.1.0+1 percent_indicator: ^4.2.2 - flutter_widget_from_html: ^0.10.3 + flutter_widget_from_html_core: ^0.10.3 shimmer: ^3.0.0 material_design_icons_flutter: ^7.0.7296 flutter_dotenv: ^5.0.2 From 05e6ae8d0eb39dc35247b2eb212ec5ce59fa3a5f Mon Sep 17 00:00:00 2001 From: bdmendes Date: Fri, 14 Jul 2023 17:08:47 +0000 Subject: [PATCH 062/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index ab41fa991..98c07b195 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.33+151 \ No newline at end of file +1.5.34+152 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 9fac03c6f..629f1ddc2 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.33+151 +version: 1.5.34+152 environment: sdk: ">=2.17.1 <3.0.0" From 6c3eee85a4ffe6ab43695212e00f158ea2213f8c Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 15 Jul 2023 16:25:08 +0100 Subject: [PATCH 063/100] Load from the db immediately --- .../providers/state_provider_notifier.dart | 37 +++++++++++++--- uni/lib/view/lazy_consumer.dart | 42 +++++++++++-------- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index c7fbe29a5..bcbf673b7 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -13,7 +13,8 @@ import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { static final Lock _lock = Lock(); RequestStatus _status; - bool _initialized; + bool _initializedFromStorage; + bool _initializedFromRemote; DateTime? _lastUpdateTime; bool dependsOnSession; Duration? cacheDuration; @@ -28,7 +29,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { RequestStatus initialStatus = RequestStatus.busy, bool initialize = true}) : _status = initialStatus, - _initialized = !initialize; + _initializedFromStorage = !initialize, + _initializedFromRemote = !initialize; Future _loadFromStorage() async { _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( @@ -102,19 +104,42 @@ abstract class StateProviderNotifier extends ChangeNotifier { }); } - Future ensureInitialized(Session session, Profile profile) async { + Future ensureInitialized(BuildContext context) async { + await ensureInitializedFromStorage(); + + if (context.mounted) { + await ensureInitializedFromRemote(context); + } + } + + Future ensureInitializedFromRemote(BuildContext context) async { await _lock.synchronized(() async { - if (_initialized) { + if (_initializedFromRemote) { return; } - _initialized = true; + _initializedFromRemote = true; + + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; - await _loadFromStorage(); await _loadFromRemote(session, profile); }); } + Future ensureInitializedFromStorage() async { + await _lock.synchronized(() async { + if (_initializedFromStorage) { + return; + } + + _initializedFromStorage = true; + await _loadFromStorage(); + }); + } + Future loadFromStorage(); Future loadFromRemote(Session session, Profile profile); diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index bc3cd460a..6a83b135f 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -19,28 +19,34 @@ class LazyConsumer extends StatelessWidget { @override Widget build(BuildContext context) { - try { - final sessionProvider = Provider.of(context); - final profileProvider = Provider.of(context); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - final session = sessionProvider.session; - final profile = profileProvider.profile; + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + // Load data stored in the database immediately final provider = Provider.of(context, listen: false); + await provider.ensureInitializedFromStorage(); + // If the provider fetchers depend on the session, make sure that + // SessionProvider and ProfileProvider are initialized if (provider.dependsOnSession) { - sessionProvider.ensureInitialized(session, profile).then((_) => - profileProvider - .ensureInitialized(session, profile) - .then((_) => provider.ensureInitialized(session, profile))); - } else { - provider.ensureInitialized(session, profile); + if (context.mounted) { + await Provider.of(context, listen: false) + .ensureInitialized(context); + } + if (context.mounted) { + await Provider.of(context, listen: false) + .ensureInitialized(context); + } + } + + // Finally, complete provider initialization + if (context.mounted) { + await provider.ensureInitializedFromRemote(context); } - }); - } catch (_) { - // The provider won't be initialized - // Should only happen in tests - } + } catch (_) { + // The provider won't be initialized + // Should only happen in tests + } + }); return Consumer(builder: (context, provider, _) { return builder(context, provider); From 09a620abb95251471f4534892fdb39aaa7db8c6f Mon Sep 17 00:00:00 2001 From: Bruno Mendes <61701401+bdmendes@users.noreply.github.com> Date: Sat, 15 Jul 2023 19:15:40 +0100 Subject: [PATCH 064/100] Notify listeners after loading from storage --- uni/lib/model/providers/state_provider_notifier.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index bcbf673b7..5c853b018 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -37,6 +37,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { runtimeType.toString()); await loadFromStorage(); + notifyListeners(); Logger().i("Loaded $runtimeType info from storage"); } From 0a79ea59b3c52d1492648d659a3e96ffa5671596 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sat, 15 Jul 2023 23:44:49 +0100 Subject: [PATCH 065/100] Expanded Image+Label pattern --- .../images/{school.png => schedule.png} | Bin .../bus_stop_next_arrivals.dart | 24 ++++++------ .../common_widgets/expanded_image_label.dart | 36 ++++++++++++++++++ uni/lib/view/exams/exams.dart | 20 +++++----- uni/lib/view/schedule/schedule.dart | 12 +++--- 5 files changed, 61 insertions(+), 31 deletions(-) rename uni/assets/images/{school.png => schedule.png} (100%) create mode 100644 uni/lib/view/common_widgets/expanded_image_label.dart diff --git a/uni/assets/images/school.png b/uni/assets/images/schedule.png similarity index 100% rename from uni/assets/images/school.png rename to uni/assets/images/schedule.png diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index fd6da89de..599fde29b 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -5,6 +5,7 @@ import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/model/providers/bus_stop_provider.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; import 'package:uni/view/common_widgets/page_title.dart'; @@ -85,21 +86,18 @@ class NextArrivalsState extends State { result.addAll(getContent(context)); } else { result.add( - Image.asset('assets/images/bus.png', height: 300, width: 300,), + ImageLabel(imagePath: 'assets/images/bus.png', label: 'Não percas nenhum autocarro', labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Theme.of(context).colorScheme.primary)) ); result.add( - const Text('Não percas nenhum autocarro', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Color.fromARGB(255, 0x75, 0x17, 0x1e))), - ); - result.add( - Container( - padding: const EdgeInsets.only(top: 15), - child: ElevatedButton( + Column( + children: [ + ElevatedButton( onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const BusStopSelectionPage())), - child: const Text('Adicionar'), - ) - )); + context, + MaterialPageRoute(builder: (context) => const BusStopSelectionPage())), + child: const Text('Adicionar'), + ), + ])); } return result; @@ -134,7 +132,7 @@ class NextArrivalsState extends State { child: Text('Não foi possível obter informação', maxLines: 2, overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.subtitle1))); + style: Theme.of(context).textTheme.titleMedium))); return result; } diff --git a/uni/lib/view/common_widgets/expanded_image_label.dart b/uni/lib/view/common_widgets/expanded_image_label.dart new file mode 100644 index 000000000..f87a1121c --- /dev/null +++ b/uni/lib/view/common_widgets/expanded_image_label.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class ImageLabel extends StatelessWidget { + final String imagePath; + final String label; + final TextStyle? labelTextStyle; + final String sublabel; + final TextStyle? sublabelTextStyle; + + const ImageLabel({Key? key, required this.imagePath, required this.label, this.labelTextStyle, this.sublabel = '', this.sublabelTextStyle}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + imagePath, + height: 300, + width: 300, + ), + const SizedBox(height: 10), + Text( + label, + style: labelTextStyle, + ), + if(sublabel.isNotEmpty) + const SizedBox(height: 20), + Text( + sublabel, + style: sublabelTextStyle, + ), + ], + ); + } +} \ No newline at end of file diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 7d72b54bd..340ac14b5 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -6,6 +6,7 @@ import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/exams/widgets/day_title.dart'; class ExamsPageView extends StatefulWidget { @@ -43,17 +44,14 @@ class ExamsPageViewState extends GeneralPageViewState { if (exams.isEmpty) { columns.add(Center( heightFactor: 1.2, - child: Column( - children: [ - Image.asset('assets/images/vacation.png', height: 300, width: 300,), - const Text('Parece que estás de férias!', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color.fromARGB(255, 0x75, 0x17, 0x1e)), - ), - const Text('\nNão tens exames marcados', - style: TextStyle(fontSize: 15), - ), - ]) - )); + child: ImageLabel(imagePath: 'assets/images/vacation.png', + label: 'Parece que estás de férias!', + labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Theme.of(context).colorScheme.primary), + sublabel: 'Não tens exames marcados', + sublabelTextStyle: const TextStyle(fontSize: 15), + ) + ) + ); return columns; } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 8fe37a05b..83f1ecc35 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -6,6 +6,7 @@ import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lecture_provider.dart'; import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; @@ -178,13 +179,10 @@ class SchedulePageViewState extends GeneralPageViewState content: aggLectures[day], contentChecker: aggLectures[day].isNotEmpty, onNullContent: Center( - child: Column( - children: [ - Image.asset('assets/images/school.png', height: 300, width: 300,), - Text('Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', style: const TextStyle( - fontSize: 15,),) - ]) - )); + child: ImageLabel(imagePath: 'assets/images/schedule.png', label: 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', labelTextStyle: const TextStyle(fontSize: 15), + ) + ) + ); } } From 225f629632738b05d3b3d44ba43ee1c35d0e0204 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sun, 16 Jul 2023 00:02:37 +0100 Subject: [PATCH 066/100] Fixing commit error --- .../bus_stop_next_arrivals.dart | 23 +++++++++++++++---- uni/lib/view/exams/exams.dart | 21 +++++++++++------ uni/lib/view/schedule/schedule.dart | 19 +++++++++------ 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 19dbdfcc4..2d1ff3a40 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; import 'package:uni/view/common_widgets/last_update_timestamp.dart'; @@ -24,8 +25,8 @@ class BusStopNextArrivalsPageState Widget getBody(BuildContext context) { return LazyConsumer( builder: (context, busProvider) => ListView(children: [ - NextArrivals(busProvider.configuredBusStops, busProvider.status) - ])); + NextArrivals(busProvider.configuredBusStops, busProvider.status) + ])); } @override @@ -74,6 +75,7 @@ class NextArrivalsState extends State { } /// Returns a list of widgets for a successfull request + List requestSuccessful(context) { final List result = []; @@ -82,8 +84,19 @@ class NextArrivalsState extends State { if (widget.buses.isNotEmpty) { result.addAll(getContent(context)); } else { - result.add(Text('Não existe nenhuma paragem configurada', - style: Theme.of(context).textTheme.titleLarge)); + result.add( + ImageLabel(imagePath: 'assets/images/bus.png', label: 'Não percas nenhum autocarro', labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Theme.of(context).colorScheme.primary)) + ); + result.add( + Column( + children: [ + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const BusStopSelectionPage())), + child: const Text('Adicionar'), + ), + ])); } return result; @@ -206,4 +219,4 @@ class NextArrivalsState extends State { return rows; } -} +} \ No newline at end of file diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 8f13c7c9d..a8b386f82 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -5,6 +5,7 @@ import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/day_title.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -28,7 +29,7 @@ class ExamsPageViewState extends GeneralPageViewState { Column( mainAxisSize: MainAxisSize.max, children: - createExamsColumn(context, examProvider.getFilteredExams()), + createExamsColumn(context, examProvider.getFilteredExams()), ) ], ); @@ -38,14 +39,20 @@ class ExamsPageViewState extends GeneralPageViewState { /// Creates a column with all the user's exams. List createExamsColumn(context, List exams) { final List columns = []; + columns.add(const ExamPageTitle()); if (exams.isEmpty) { columns.add(Center( - heightFactor: 2, - child: Text('Não possui exames marcados.', - style: Theme.of(context).textTheme.titleLarge), - )); + heightFactor: 1.2, + child: ImageLabel(imagePath: 'assets/images/vacation.png', + label: 'Parece que estás de férias!', + labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Theme.of(context).colorScheme.primary), + sublabel: 'Não tens exames marcados', + sublabelTextStyle: const TextStyle(fontSize: 15), + ) + ) + ); return columns; } @@ -107,7 +114,7 @@ class ExamsPageViewState extends GeneralPageViewState { Widget createExamContext(context, Exam exam) { final isHidden = - Provider.of(context).hiddenExams.contains(exam.id); + Provider.of(context).hiddenExams.contains(exam.id); return Container( key: Key('$exam-exam'), margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), @@ -123,4 +130,4 @@ class ExamsPageViewState extends GeneralPageViewState { return Provider.of(context, listen: false) .forceRefresh(context); } -} +} \ No newline at end of file diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 42de00009..04e0830fd 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -9,6 +9,7 @@ import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class SchedulePage extends StatefulWidget { @@ -44,7 +45,7 @@ class SchedulePageView extends StatefulWidget { final int weekDay = DateTime.now().weekday; static final List daysOfTheWeek = - TimeString.getWeekdaysStrings(includeWeekend: false); + TimeString.getWeekdaysStrings(includeWeekend: false); static List> groupLecturesByDay(schedule) { final aggLectures = >[]; @@ -104,10 +105,10 @@ class SchedulePageViewState extends GeneralPageViewState ), Expanded( child: TabBarView( - controller: tabController, - children: + controller: tabController, + children: createSchedule(context, widget.lectures, widget.scheduleStatus), - )) + )) ]); } @@ -171,9 +172,10 @@ class SchedulePageViewState extends GeneralPageViewState status: scheduleStatus ?? RequestStatus.none, builder: () => dayColumnBuilder(day, aggLectures[day], context), hasContentPredicate: aggLectures[day].isNotEmpty, - onNullContent: Center( - child: Text( - 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.')), + onNullContent: Center( + child: ImageLabel(imagePath: 'assets/images/schedule.png', label: 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', labelTextStyle: const TextStyle(fontSize: 15), + ) + ) ); } @@ -183,3 +185,6 @@ class SchedulePageViewState extends GeneralPageViewState .forceRefresh(context); } } + + + From a398aa0c357ef5ffb0ad595cc98c6872b31e780f Mon Sep 17 00:00:00 2001 From: thePeras Date: Mon, 17 Jul 2023 12:10:48 +0100 Subject: [PATCH 067/100] Added original ni logo --- uni/assets/images/logo_ni.svg | 15 +++++++++++++++ uni/lib/view/about/about.dart | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 uni/assets/images/logo_ni.svg diff --git a/uni/assets/images/logo_ni.svg b/uni/assets/images/logo_ni.svg new file mode 100644 index 000000000..1b3f6776d --- /dev/null +++ b/uni/assets/images/logo_ni.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/uni/lib/view/about/about.dart b/uni/lib/view/about/about.dart index 411a1901d..4fbf23e65 100644 --- a/uni/lib/view/about/about.dart +++ b/uni/lib/view/about/about.dart @@ -18,9 +18,7 @@ class AboutPageViewState extends GeneralPageViewState { return ListView( children: [ SvgPicture.asset( - 'assets/images/ni_logo.svg', - colorFilter: - ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), + 'assets/images/logo_ni.svg', width: queryData.size.height / 7, height: queryData.size.height / 7, ), From f1ff8b7ba73ff979b8d318df9d5b9c39c4bc2fff Mon Sep 17 00:00:00 2001 From: thePeras Date: Mon, 17 Jul 2023 12:14:58 +0100 Subject: [PATCH 068/100] Remove unused assets --- uni/assets/images/logo_ni_original.svg | 53 ----------------------- uni/assets/images/ni_logo.svg | 58 -------------------------- uni/assets/images/outline_red.svg | 51 ---------------------- 3 files changed, 162 deletions(-) delete mode 100644 uni/assets/images/logo_ni_original.svg delete mode 100644 uni/assets/images/ni_logo.svg delete mode 100644 uni/assets/images/outline_red.svg diff --git a/uni/assets/images/logo_ni_original.svg b/uni/assets/images/logo_ni_original.svg deleted file mode 100644 index 3130746fe..000000000 --- a/uni/assets/images/logo_ni_original.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - -logo2 - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/uni/assets/images/ni_logo.svg b/uni/assets/images/ni_logo.svg deleted file mode 100644 index a382c6f0e..000000000 --- a/uni/assets/images/ni_logo.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/uni/assets/images/outline_red.svg b/uni/assets/images/outline_red.svg deleted file mode 100644 index 0ca3d1ce9..000000000 --- a/uni/assets/images/outline_red.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 8ef3ed452c6b57c695ac6cf0bab1d083453bbf5e Mon Sep 17 00:00:00 2001 From: bdmendes Date: Mon, 17 Jul 2023 11:57:36 +0000 Subject: [PATCH 069/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 98c07b195..dee68838b 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.34+152 \ No newline at end of file +1.5.35+153 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 629f1ddc2..64863aaf9 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.34+152 +version: 1.5.35+153 environment: sdk: ">=2.17.1 <3.0.0" From 9ac5e96c66a99feebc076db94e3bbdc32eb58b2f Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 18 Jul 2023 14:57:52 +0100 Subject: [PATCH 070/100] Cleanup login logic --- .../background_workers/notifications.dart | 13 +- .../current_course_units_fetcher.dart | 2 +- .../controller/fetchers/courses_fetcher.dart | 2 +- uni/lib/controller/fetchers/fees_fetcher.dart | 2 +- .../controller/fetchers/print_fetcher.dart | 4 +- .../controller/fetchers/profile_fetcher.dart | 2 +- .../fetchers/reference_fetcher.dart | 10 +- .../schedule_fetcher_api.dart | 2 +- uni/lib/controller/logout.dart | 2 +- .../controller/networking/network_router.dart | 164 ++++++++---------- uni/lib/model/entities/session.dart | 52 +++--- .../providers/startup/profile_provider.dart | 2 +- .../providers/startup/session_provider.dart | 87 ++++------ .../providers/state_provider_notifier.dart | 9 + .../widgets/course_unit_classes.dart | 3 +- uni/lib/view/login/login.dart | 58 +++---- uni/lib/view/splash/splash.dart | 8 +- uni/test/integration/src/exams_page_test.dart | 4 +- .../integration/src/schedule_page_test.dart | 2 +- .../unit/providers/exams_provider_test.dart | 2 +- .../unit/providers/lecture_provider_test.dart | 2 +- 21 files changed, 199 insertions(+), 233 deletions(-) diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index 00d0fdef2..d614116db 100644 --- a/uni/lib/controller/background_workers/notifications.dart +++ b/uni/lib/controller/background_workers/notifications.dart @@ -60,15 +60,20 @@ class NotificationManager { } static Future updateAndTriggerNotifications() async { - //first we get the .json file that contains the last time that the notification have ran - _initFlutterNotificationsPlugin(); - final notificationStorage = await NotificationTimeoutStorage.create(); final userInfo = await AppSharedPreferences.getPersistentUserInfo(); final faculties = await AppSharedPreferences.getUserFaculties(); - final Session session = await NetworkRouter.login( + final Session? session = await NetworkRouter.login( userInfo.item1, userInfo.item2, faculties, false); + if (session == null) { + return; + } + + // Get the .json file that contains the last time that the notification has ran + _initFlutterNotificationsPlugin(); + final notificationStorage = await NotificationTimeoutStorage.create(); + for (Notification Function() value in notificationMap.values) { final Notification notification = value(); final DateTime lastRan = notificationStorage diff --git a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart index 9b234d626..7160c5369 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart @@ -18,7 +18,7 @@ class CurrentCourseUnitsFetcher implements SessionDependantFetcher { Future> getCurrentCourseUnits(Session session) async { final String url = getEndpoints(session)[0]; final Response response = await NetworkRouter.getWithCookies( - url, {'pv_codigo': session.studentNumber}, session); + url, {'pv_codigo': session.username}, session); if (response.statusCode == 200) { final responseBody = json.decode(response.body); final List ucs = []; diff --git a/uni/lib/controller/fetchers/courses_fetcher.dart b/uni/lib/controller/fetchers/courses_fetcher.dart index a864d1059..2e0f50808 100644 --- a/uni/lib/controller/fetchers/courses_fetcher.dart +++ b/uni/lib/controller/fetchers/courses_fetcher.dart @@ -18,7 +18,7 @@ class CoursesFetcher implements SessionDependantFetcher { final urls = getEndpoints(session); return urls .map((url) => NetworkRouter.getWithCookies( - url, {'pv_num_unico': session.studentNumber}, session)) + url, {'pv_num_unico': session.username}, session)) .toList(); } } diff --git a/uni/lib/controller/fetchers/fees_fetcher.dart b/uni/lib/controller/fetchers/fees_fetcher.dart index a7c3c7f4d..edd225aae 100644 --- a/uni/lib/controller/fetchers/fees_fetcher.dart +++ b/uni/lib/controller/fetchers/fees_fetcher.dart @@ -15,7 +15,7 @@ class FeesFetcher implements SessionDependantFetcher { Future getUserFeesResponse(Session session) { final String url = getEndpoints(session)[0]; - final Map query = {'pct_cod': session.studentNumber}; + final Map query = {'pct_cod': session.username}; return NetworkRouter.getWithCookies(url, query, session); } } diff --git a/uni/lib/controller/fetchers/print_fetcher.dart b/uni/lib/controller/fetchers/print_fetcher.dart index 763bea773..9907dac03 100644 --- a/uni/lib/controller/fetchers/print_fetcher.dart +++ b/uni/lib/controller/fetchers/print_fetcher.dart @@ -13,7 +13,7 @@ class PrintFetcher implements SessionDependantFetcher { getUserPrintsResponse(Session session) { final String url = getEndpoints(session)[0]; - final Map query = {'p_codigo': session.studentNumber}; + final Map query = {'p_codigo': session.username}; return NetworkRouter.getWithCookies(url, query, session); } @@ -26,7 +26,7 @@ class PrintFetcher implements SessionDependantFetcher { final Map data = { 'p_tipo_id': '3', - 'pct_codigo': session.studentNumber, + 'pct_codigo': session.username, 'p_valor': '1', 'p_valor_livre': amount.toStringAsFixed(2).trim().replaceAll('.', ',') }; diff --git a/uni/lib/controller/fetchers/profile_fetcher.dart b/uni/lib/controller/fetchers/profile_fetcher.dart index c57bed68e..d15a4ef1d 100644 --- a/uni/lib/controller/fetchers/profile_fetcher.dart +++ b/uni/lib/controller/fetchers/profile_fetcher.dart @@ -21,7 +21,7 @@ class ProfileFetcher implements SessionDependantFetcher { final url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}mob_fest_geral.perfil?'; final response = await NetworkRouter.getWithCookies( - url, {'pv_codigo': session.studentNumber}, session); + url, {'pv_codigo': session.username}, session); if (response.statusCode == 200) { final Profile profile = Profile.fromResponse(response); diff --git a/uni/lib/controller/fetchers/reference_fetcher.dart b/uni/lib/controller/fetchers/reference_fetcher.dart index 8e54ae85d..f1258cc6a 100644 --- a/uni/lib/controller/fetchers/reference_fetcher.dart +++ b/uni/lib/controller/fetchers/reference_fetcher.dart @@ -4,11 +4,11 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/session.dart'; class ReferenceFetcher implements SessionDependantFetcher { - @override List getEndpoints(Session session) { - final List baseUrls = NetworkRouter.getBaseUrlsFromSession(session) - + [NetworkRouter.getBaseUrl('sasup')]; + final List baseUrls = + NetworkRouter.getBaseUrlsFromSession(session) + + [NetworkRouter.getBaseUrl('sasup')]; final List urls = baseUrls .map((url) => '${url}gpag_ccorrente_geral.conta_corrente_view') .toList(); @@ -18,7 +18,7 @@ class ReferenceFetcher implements SessionDependantFetcher { Future getUserReferenceResponse(Session session) { final List urls = getEndpoints(session); final String url = urls[0]; - final Map query = {'pct_cod': session.studentNumber}; + final Map query = {'pct_cod': session.username}; return NetworkRouter.getWithCookies(url, query, session); } -} \ No newline at end of file +} diff --git a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart index f6ac80f8d..71b60501f 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart @@ -25,7 +25,7 @@ class ScheduleFetcherApi extends ScheduleFetcher { final response = await NetworkRouter.getWithCookies( url, { - 'pv_codigo': session.studentNumber, + 'pv_codigo': session.username, 'pv_semana_ini': dates.beginWeek, 'pv_semana_fim': dates.endWeek }, diff --git a/uni/lib/controller/logout.dart b/uni/lib/controller/logout.dart index 65e4d3965..3f7c6b473 100644 --- a/uni/lib/controller/logout.dart +++ b/uni/lib/controller/logout.dart @@ -29,7 +29,7 @@ Future logout(BuildContext context) async { AppLastUserInfoUpdateDatabase().deleteLastUpdate(); AppBusStopDatabase().deleteBusStops(); AppCourseUnitsDatabase().deleteCourseUnits(); - NetworkRouter.killAuthentication(faculties); + NetworkRouter.killSigarraAuthentication(faculties); final path = (await getApplicationDocumentsDirectory()).path; final directory = Directory(path); diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index f92b85e7b..ff04a25ee 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; @@ -16,116 +15,98 @@ extension UriString on String { /// Manages the networking of the app. class NetworkRouter { + /// The HTTP client used for all requests. + /// Can be set to null to use the default client. + /// This is useful for testing. static http.Client? httpClient; + + /// The timeout for Sigarra login requests. static const int loginRequestTimeout = 20; - static Lock loginLock = Lock(); - /// Creates an authenticated [Session] on the given [faculty] with the - /// given username [user] and password [pass]. - static Future login(String user, String pass, List faculties, - bool persistentSession) async { - final String url = - '${NetworkRouter.getBaseUrls(faculties)[0]}mob_val_geral.autentica'; - final http.Response response = await http.post(url.toUri(), body: { - 'pv_login': user, - 'pv_password': pass - }).timeout(const Duration(seconds: loginRequestTimeout)); - if (response.statusCode == 200) { - final Session session = Session.fromLogin(response, faculties); - session.persistentSession = persistentSession; - Logger().i('Login successful'); - return session; - } else { - Logger().e('Login failed: ${response.body}'); - - return Session( - authenticated: false, - faculties: faculties, - studentNumber: '', - cookies: '', - type: '', - persistentSession: false); - } - } + /// The mutual exclusion primitive for login requests. + static Lock loginLock = Lock(); - /// Determines if a re-login with the [session] is possible. - static Future reLogin(Session session) { + /// Performs a login using the Sigarra API, + /// returning an authenticated [Session] on the given [faculties] with the + /// given username [username] and password [password] if successful. + static Future login(String username, String password, + List faculties, bool persistentSession) async { return loginLock.synchronized(() async { - if (!session.persistentSession) { - return false; + final String url = + '${NetworkRouter.getBaseUrls(faculties)[0]}mob_val_geral.autentica'; + + final http.Response response = await http.post(url.toUri(), body: { + 'pv_login': username, + 'pv_password': password + }).timeout(const Duration(seconds: loginRequestTimeout)); + + if (response.statusCode != 200) { + Logger().e("Login failed with status code ${response.statusCode}"); + return null; } - if (session.loginRequest != null) { - return session.loginRequest!; - } else { - return session.loginRequest = loginFromSession(session).then((_) { - session.loginRequest = null; - return true; - }); + final Session? session = + Session.fromLogin(response, faculties, persistentSession); + if (session == null) { + Logger().e('Login failed: user not authenticated'); + return null; } + + Logger().i('Login successful'); + return session; }); } - /// Re-authenticates the user [session]. - static Future loginFromSession(Session session) async { - Logger().i('Trying to login...'); - final String url = - '${NetworkRouter.getBaseUrls(session.faculties)[0]}mob_val_geral.autentica'; - final http.Response response = await http.post(url.toUri(), body: { - 'pv_login': session.studentNumber, - 'pv_password': await AppSharedPreferences.getUserPassword(), - }).timeout(const Duration(seconds: loginRequestTimeout)); - final responseBody = json.decode(response.body); - if (response.statusCode == 200 && responseBody['authenticated']) { - session.authenticated = true; - session.studentNumber = responseBody['codigo']; - session.type = responseBody['tipo']; - session.cookies = NetworkRouter.extractCookies(response.headers); - Logger().i('Re-login successful'); - return true; - } else { - Logger().e('Re-login failed'); - return false; - } + /// Re-authenticates the user via the Sigarra API + /// using data stored in [session], + /// returning an updated Session if successful. + static Future reLoginFromSession(Session session) async { + final String username = session.username; + final String password = await AppSharedPreferences.getUserPassword(); + final List faculties = session.faculties; + final bool persistentSession = session.persistentSession; + + return await login(username, password, faculties, persistentSession); } /// Returns the response body of the login in Sigarra /// given username [user] and password [pass]. static Future loginInSigarra( String user, String pass, List faculties) async { - final String url = - '${NetworkRouter.getBaseUrls(faculties)[0]}vld_validacao.validacao'; + return loginLock.synchronized(() async { + final String url = + '${NetworkRouter.getBaseUrls(faculties)[0]}vld_validacao.validacao'; - final response = await http.post(url.toUri(), body: { - 'p_user': user, - 'p_pass': pass - }).timeout(const Duration(seconds: loginRequestTimeout)); + final response = await http.post(url.toUri(), body: { + 'p_user': user, + 'p_pass': pass + }).timeout(const Duration(seconds: loginRequestTimeout)); - return response.body; + return response.body; + }); } /// Extracts the cookies present in [headers]. - static String extractCookies(dynamic headers) { + static String extractCookies(Map headers) { final List cookieList = []; - final String cookies = headers['set-cookie']; - if (cookies != '') { + final String? cookies = headers['set-cookie']; + + if (cookies != null && cookies != '') { final List rawCookies = cookies.split(','); for (var c in rawCookies) { cookieList.add(Cookie.fromSetCookieValue(c).toString()); } } + return cookieList.join(';'); } /// Makes an authenticated GET request with the given [session] to the /// resource located at [url] with the given [query] parameters. + /// If the request fails with a 403 status code, the user is re-authenticated + /// and the session is updated. static Future getWithCookies( String baseUrl, Map query, Session session) async { - final loginSuccessful = await session.loginRequest; - if (loginSuccessful != null && !loginSuccessful) { - return Future.error('Login failed'); - } - if (!baseUrl.contains('?')) { baseUrl += '?'; } @@ -143,29 +124,33 @@ class NetworkRouter { final http.Response response = await (httpClient != null ? httpClient!.get(url.toUri(), headers: headers) : http.get(url.toUri(), headers: headers)); + if (response.statusCode == 200) { return response; - } else if (response.statusCode == 403 && !(await userLoggedIn(session))) { - // HTTP403 - Forbidden - final bool reLoginSuccessful = await reLogin(session); - if (reLoginSuccessful) { - headers['cookie'] = session.cookies; - return http.get(url.toUri(), headers: headers); - } else { + } + + final forbidden = response.statusCode == 403; + if (forbidden && !(await userLoggedIn(session))) { + final Session? newSession = await reLoginFromSession(session); + + if (newSession == null) { NavigationService.logout(); - Logger().e('Login failed'); return Future.error('Login failed'); } - } else { - return Future.error('HTTP Error ${response.statusCode}'); + + session.cookies = newSession.cookies; + headers['cookie'] = session.cookies; + return http.get(url.toUri(), headers: headers); } + + return Future.error('HTTP Error: ${response.statusCode}'); } /// Check if the user is still logged in, /// performing a health check on the user's personal page. static Future userLoggedIn(Session session) async { final url = - '${getBaseUrl(session.faculties[0])}fest_geral.cursos_list?pv_num_unico=${session.studentNumber}'; + '${getBaseUrl(session.faculties[0])}fest_geral.cursos_list?pv_num_unico=${session.username}'; final Map headers = {}; headers['cookie'] = session.cookies; final http.Response response = await (httpClient != null @@ -190,16 +175,19 @@ class NetworkRouter { } /// Makes an HTTP request to terminate the session in Sigarra. - static Future killAuthentication(List faculties) async { + static Future killSigarraAuthentication(List faculties) async { final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; + final response = await http .get(url.toUri()) .timeout(const Duration(seconds: loginRequestTimeout)); + if (response.statusCode == 200) { Logger().i("Logout Successful"); } else { Logger().i("Logout Failed"); } + return response; } } diff --git a/uni/lib/model/entities/session.dart b/uni/lib/model/entities/session.dart index a78e9810e..f22546a19 100644 --- a/uni/lib/model/entities/session.dart +++ b/uni/lib/model/entities/session.dart @@ -1,47 +1,35 @@ import 'dart:convert'; import 'package:uni/controller/networking/network_router.dart'; +import 'package:http/http.dart' as http; /// Stores information about a user session. class Session { - /// Whether or not the user is authenticated. - bool authenticated; - bool persistentSession; - List faculties; - String type; + String username; String cookies; - String studentNumber; - Future? - loginRequest; // TODO: accessed directly in Network Router; change the logic + List faculties; + bool persistentSession; Session( - {this.authenticated = false, - this.studentNumber = '', - this.type = '', - this.cookies = '', - this.faculties = const [''], + {required this.username, + required this.cookies, + required this.faculties, this.persistentSession = false}); - /// Creates a new instance from an HTTP response - /// to login in one of the faculties. - static Session fromLogin(dynamic response, List faculties) { + /// Creates a new Session instance from an HTTP response. + /// Returns null if the authentication failed. + static Session? fromLogin( + http.Response response, List faculties, bool persistentSession) { final responseBody = json.decode(response.body); - if (responseBody['authenticated']) { - return Session( - authenticated: true, - faculties: faculties, - studentNumber: responseBody['codigo'], - type: responseBody['tipo'], - cookies: NetworkRouter.extractCookies(response.headers), - persistentSession: false); - } else { - return Session( - authenticated: false, - faculties: faculties, - type: '', - cookies: '', - studentNumber: '', - persistentSession: false); + + if (!responseBody['authenticated']) { + return null; } + + return Session( + faculties: faculties, + username: responseBody['codigo'], + cookies: NetworkRouter.extractCookies(response.headers), + persistentSession: false); } } diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index ca37ec3e0..21d547997 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -236,7 +236,7 @@ class ProfileProvider extends StateProviderNotifier { static Future fetchOrGetCachedProfilePicture( int? studentNumber, Session session, {forceRetrieval = false}) { - studentNumber ??= int.parse(session.studentNumber.replaceAll("up", "")); + studentNumber ??= int.parse(session.username.replaceAll("up", "")); final String faculty = session.faculties[0]; final String url = diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 0866cb720..8083f0372 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -11,11 +11,10 @@ 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/view/navigation_service.dart'; class SessionProvider extends StateProviderNotifier { - Session _session = Session(); - List _faculties = []; + late Session _session; + late List _faculties; SessionProvider() : super( @@ -26,7 +25,7 @@ class SessionProvider extends StateProviderNotifier { Session get session => _session; UnmodifiableListView get faculties => - UnmodifiableListView(_faculties); + UnmodifiableListView(_faculties); @override Future loadFromStorage() async {} @@ -36,26 +35,25 @@ class SessionProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } - login(Completer action, String username, String password, - List faculties, persistentSession) async { + void restoreSession( + String username, String password, List faculties) { + _session = Session( + faculties: faculties, + username: username, + cookies: "", + persistentSession: true); + } + + Future postAuthentication(Completer action, String username, + String password, List faculties, persistentSession) async { try { updateStatus(RequestStatus.busy); - _faculties = faculties; - _session = await NetworkRouter.login( - username, password, faculties, persistentSession); - if (_session.authenticated) { - if (persistentSession) { - await AppSharedPreferences.savePersistentUserInfo( - username, password, faculties); - } - Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + final session = await NetworkRouter.login( + username, password, faculties, persistentSession); - await acceptTermsAndConditions(); - updateStatus(RequestStatus.successful); - } else { + if (session == null) { final String responseHtml = await NetworkRouter.loginInSigarra(username, password, faculties); if (isPasswordExpired(responseHtml)) { @@ -64,48 +62,27 @@ class SessionProvider extends StateProviderNotifier { action.completeError(WrongCredentialsException()); } updateStatus(RequestStatus.failed); + action.complete(); + return; } - } catch (e) { - // No internet connection or server down - action.completeError(InternetStatusException()); - updateStatus(RequestStatus.failed); - } - notifyListeners(); - action.complete(); - } - - reLogin(String username, String password, List faculties, - {Completer? action}) async { - try { - updateStatus(RequestStatus.busy); - _session = await NetworkRouter.login(username, password, faculties, true); + _session = session; - if (session.authenticated) { - Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); - updateStatus(RequestStatus.successful); - action?.complete(); - } else { - handleFailedReLogin(action); + if (persistentSession) { + await AppSharedPreferences.savePersistentUserInfo( + username, password, faculties); } - } catch (e) { - _session = Session( - studentNumber: username, - authenticated: false, - faculties: faculties, - type: '', - cookies: '', - persistentSession: true); - handleFailedReLogin(action); - } - } + Future.delayed(const Duration(seconds: 20), + () => {NotificationManager().initializeNotifications()}); - handleFailedReLogin(Completer? action) { - action?.completeError(RequestStatus.failed); - if (!session.persistentSession) { - return NavigationService.logout(); + await acceptTermsAndConditions(); + updateStatus(RequestStatus.successful); + action.complete(); + } catch (e) { + // No internet connection or server down + action.completeError(InternetStatusException()); + updateStatus(RequestStatus.failed); } } } diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index c7fbe29a5..5a93811dc 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -115,7 +115,16 @@ abstract class StateProviderNotifier extends ChangeNotifier { }); } + /// Loads data from storage into the provider. + /// This will run once when the provider is first initialized. + /// If the data is not available in storage, this method should do nothing. Future loadFromStorage(); + /// 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. Future loadFromRemote(Session session, Profile profile); } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart index a12e29a6c..211843c1d 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -19,8 +19,7 @@ class CourseUnitClassesView extends StatelessWidget { final bool isMyClass = courseUnitClass.students .where((student) => student.number == - (int.tryParse( - session.studentNumber.replaceAll(RegExp(r"\D"), "")) ?? + (int.tryParse(session.username.replaceAll(RegExp(r"\D"), "")) ?? 0)) .isNotEmpty; cards.add(CourseUnitInfoCard( diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 848247506..900a5b426 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -54,7 +54,8 @@ class LoginPageViewState extends State { final pass = passwordController.text.trim(); final completer = Completer(); - sessionProvider.login(completer, user, pass, faculties, _keepSignedIn); + sessionProvider.postAuthentication( + completer, user, pass, faculties, _keepSignedIn); completer.future.then((_) { handleLogin(sessionProvider.status, context); @@ -116,35 +117,36 @@ class LoginPageViewState extends State { left: queryData.size.width / 8, right: queryData.size.width / 8), child: ListView( - children: getWidgets(themeContext, queryData), + children: [ + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 20)), + createTitle(queryData, context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35)), + getLoginForm(queryData, context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35)), + createForgetPasswordLink(context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 15)), + createLogInButton(queryData, context, _login), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35)), + createStatusWidget(context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35)), + createSafeLoginButton(context), + ], )), onWillPop: () => onWillPop(themeContext))))); } - List getWidgets(BuildContext context, MediaQueryData queryData) { - final List widgets = []; - - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 20))); - widgets.add(createTitle(queryData, context)); - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35))); - widgets.add(getLoginForm(queryData, context)); - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35))); - widgets.add(createForgetPasswordLink(context)); - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 15))); - widgets.add(createLogInButton(queryData, context, _login)); - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35))); - widgets.add(createStatusWidget(context)); - widgets.add( - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35))); - widgets.add(createSafeLoginButton(context)); - return widgets; - } - /// Delay time before the user leaves the app Future exitAppWaiter() async { _exitApp = true; @@ -236,9 +238,7 @@ class LoginPageViewState extends State { } void handleLogin(RequestStatus? status, BuildContext context) { - final session = - Provider.of(context, listen: false).session; - if (status == RequestStatus.successful && session.authenticated) { + if (status == RequestStatus.successful) { Navigator.pushReplacementNamed( context, '/${DrawerItem.navPersonalArea.title}'); } diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index 5b0ad0d22..aec6b9a57 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -121,9 +121,9 @@ class SplashScreenState extends State { } Future getTermsAndConditions( - String userName, String password, StateProviders stateProviders) async { + String username, String password, StateProviders stateProviders) async { final completer = Completer(); - await TermsAndConditionDialog.build(context, completer, userName, password); + await TermsAndConditionDialog.build(context, completer, username, password); final state = await completer.future; switch (state) { @@ -131,8 +131,8 @@ class SplashScreenState extends State { if (mounted) { final List faculties = await AppSharedPreferences.getUserFaculties(); - await stateProviders.sessionProvider - .reLogin(userName, password, faculties); + stateProviders.sessionProvider + .restoreSession(username, password, faculties); } return MaterialPageRoute(builder: (context) => const HomePageView()); diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index 9dfaa0aa2..fdcafb977 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -89,7 +89,7 @@ void main() { ParserExams(), const Tuple2('', ''), profile, - Session(authenticated: true), + Session(username: '', cookies: '', faculties: []), [sopeCourseUnit, sdisCourseUnit]); await completer.future; @@ -128,7 +128,7 @@ void main() { ParserExams(), const Tuple2('', ''), profile, - Session(authenticated: true), + Session(username: '', cookies: '', faculties: []), [sopeCourseUnit, sdisCourseUnit]); await completer.future; diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index 7451d4b49..a4199f552 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -65,7 +65,7 @@ void main() { final Completer completer = Completer(); scheduleProvider.fetchUserLectures(completer, const Tuple2('', ''), - Session(authenticated: true), profile); + Session(username: '', cookies: '', faculties: []), profile); await completer.future; await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 9be691f5b..014a0bd50 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -47,7 +47,7 @@ void main() { final profile = Profile(); profile.courses = [Course(id: 7474)]; - final session = Session(authenticated: true); + final session = Session(username: '', cookies: '', faculties: []); final userUcs = [sopeCourseUnit, sdisCourseUnit]; NetworkRouter.httpClient = mockClient; diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index d05496119..93c2c2293 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -23,7 +23,7 @@ void main() { const Tuple2 userPersistentInfo = Tuple2('', ''); final profile = Profile(); profile.courses = [Course(id: 7474)]; - final session = Session(authenticated: true); + final session = Session(username: '', cookies: '', faculties: []); final day = DateTime(2021, 06, 01); final lecture1 = Lecture.fromHtml( From fd964d5ab2034e9eed8e14787ef6bdccbe249160 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 18 Jul 2023 15:08:38 +0100 Subject: [PATCH 071/100] Lock login methods --- .../controller/networking/network_router.dart | 43 ++++++++++--------- .../providers/state_provider_notifier.dart | 3 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index ff04a25ee..28de21133 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -149,14 +149,16 @@ class NetworkRouter { /// Check if the user is still logged in, /// performing a health check on the user's personal page. static Future userLoggedIn(Session session) async { - final url = - '${getBaseUrl(session.faculties[0])}fest_geral.cursos_list?pv_num_unico=${session.username}'; - final Map headers = {}; - headers['cookie'] = session.cookies; - final http.Response response = await (httpClient != null - ? httpClient!.get(url.toUri(), headers: headers) - : http.get(url.toUri(), headers: headers)); - return response.statusCode == 200; + return loginLock.synchronized(() async { + final url = + '${getBaseUrl(session.faculties[0])}fest_geral.cursos_list?pv_num_unico=${session.username}'; + final Map headers = {}; + headers['cookie'] = session.cookies; + final http.Response response = await (httpClient != null + ? httpClient!.get(url.toUri(), headers: headers) + : http.get(url.toUri(), headers: headers)); + return response.statusCode == 200; + }); } /// Returns the base url of the user's faculties. @@ -176,18 +178,19 @@ class NetworkRouter { /// Makes an HTTP request to terminate the session in Sigarra. static Future killSigarraAuthentication(List faculties) async { - final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; - - final response = await http - .get(url.toUri()) - .timeout(const Duration(seconds: loginRequestTimeout)); - - if (response.statusCode == 200) { - Logger().i("Logout Successful"); - } else { - Logger().i("Logout Failed"); - } + return loginLock.synchronized(() async { + final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; + final response = await http + .get(url.toUri()) + .timeout(const Duration(seconds: loginRequestTimeout)); + + if (response.statusCode == 200) { + Logger().i("Logout Successful"); + } else { + Logger().i("Logout Failed"); + } - return response; + return response; + }); } } diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 5a93811dc..95954b5a5 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -64,7 +64,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { } } else { Logger().i( - "Last info for $runtimeType is within cache period ($cacheDuration); skipping remote load"); + "Last info for $runtimeType is within cache period (last updated on $_lastUpdateTime); " + "skipping remote load"); } if (!shouldReload || !hasConnectivity || _status == RequestStatus.busy) { From a07c3ff6927a01b3328ccbdf334a764345b427c3 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 18 Jul 2023 16:08:08 +0100 Subject: [PATCH 072/100] Fix tests and document 403 retry --- .../controller/networking/network_router.dart | 34 +++++++++++++------ uni/lib/model/entities/session.dart | 4 ++- uni/test/integration/src/exams_page_test.dart | 4 +-- .../integration/src/schedule_page_test.dart | 2 +- .../unit/providers/exams_provider_test.dart | 2 +- .../unit/providers/lecture_provider_test.dart | 2 +- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index 28de21133..858683587 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -66,6 +66,8 @@ class NetworkRouter { final List faculties = session.faculties; final bool persistentSession = session.persistentSession; + Logger().i('Re-logging in user $username'); + return await login(username, password, faculties, persistentSession); } @@ -130,17 +132,29 @@ class NetworkRouter { } final forbidden = response.statusCode == 403; - if (forbidden && !(await userLoggedIn(session))) { - final Session? newSession = await reLoginFromSession(session); - - if (newSession == null) { - NavigationService.logout(); - return Future.error('Login failed'); + if (forbidden) { + final userIsLoggedIn = await userLoggedIn(session); + if (!userIsLoggedIn) { + final Session? newSession = await reLoginFromSession(session); + + if (newSession == null) { + NavigationService.logout(); + return Future.error('Login failed'); + } + + session.cookies = newSession.cookies; + headers['cookie'] = session.cookies; + return http.get(url.toUri(), headers: headers); + } else { + // If the user is logged in but still got a 403, they are forbidden to access the resource + // or the login was invalid at the time of the request, but other thread re-authenticated. + // Since we do not know which one is the case, we try again. + headers['cookie'] = session.cookies; + final response = await http.get(url.toUri(), headers: headers); + return response.statusCode == 200 + ? Future.value(response) + : Future.error('HTTP Error: ${response.statusCode}'); } - - session.cookies = newSession.cookies; - headers['cookie'] = session.cookies; - return http.get(url.toUri(), headers: headers); } return Future.error('HTTP Error: ${response.statusCode}'); diff --git a/uni/lib/model/entities/session.dart b/uni/lib/model/entities/session.dart index f22546a19..7108ddd89 100644 --- a/uni/lib/model/entities/session.dart +++ b/uni/lib/model/entities/session.dart @@ -14,7 +14,9 @@ class Session { {required this.username, required this.cookies, required this.faculties, - this.persistentSession = false}); + this.persistentSession = false}) { + assert(faculties.isNotEmpty); + } /// Creates a new Session instance from an HTTP response. /// Returns null if the authentication failed. diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index fdcafb977..162cc90d4 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -89,7 +89,7 @@ void main() { ParserExams(), const Tuple2('', ''), profile, - Session(username: '', cookies: '', faculties: []), + Session(username: '', cookies: '', faculties: ['feup']), [sopeCourseUnit, sdisCourseUnit]); await completer.future; @@ -128,7 +128,7 @@ void main() { ParserExams(), const Tuple2('', ''), profile, - Session(username: '', cookies: '', faculties: []), + Session(username: '', cookies: '', faculties: ['feup']), [sopeCourseUnit, sdisCourseUnit]); await completer.future; diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index a4199f552..93328b60c 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -65,7 +65,7 @@ void main() { final Completer completer = Completer(); scheduleProvider.fetchUserLectures(completer, const Tuple2('', ''), - Session(username: '', cookies: '', faculties: []), profile); + Session(username: '', cookies: '', faculties: ['feup']), profile); await completer.future; await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 014a0bd50..e0cd3072d 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -47,7 +47,7 @@ void main() { final profile = Profile(); profile.courses = [Course(id: 7474)]; - final session = Session(username: '', cookies: '', faculties: []); + final session = Session(username: '', cookies: '', faculties: ['feup']); final userUcs = [sopeCourseUnit, sdisCourseUnit]; NetworkRouter.httpClient = mockClient; diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 93c2c2293..c3f604694 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -23,7 +23,7 @@ void main() { const Tuple2 userPersistentInfo = Tuple2('', ''); final profile = Profile(); profile.courses = [Course(id: 7474)]; - final session = Session(username: '', cookies: '', faculties: []); + final session = Session(username: '', cookies: '', faculties: ['feup']); final day = DateTime(2021, 06, 01); final lecture1 = Lecture.fromHtml( From 285d6ecfbec3bb70a4e77b01979a9faea44470e7 Mon Sep 17 00:00:00 2001 From: thePeras Date: Thu, 20 Jul 2023 20:31:19 +0100 Subject: [PATCH 073/100] Implemented job to check if code is formated --- .../{test_lint.yaml => format_lint_test.yaml} | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) rename .github/workflows/{test_lint.yaml => format_lint_test.yaml} (57%) diff --git a/.github/workflows/test_lint.yaml b/.github/workflows/format_lint_test.yaml similarity index 57% rename from .github/workflows/test_lint.yaml rename to .github/workflows/format_lint_test.yaml index ffb255569..b6377416b 100644 --- a/.github/workflows/test_lint.yaml +++ b/.github/workflows/format_lint_test.yaml @@ -1,9 +1,28 @@ on: pull_request +env: + FLUTTER_VERSION: 3.7.2 + JAVA_VERSION: 11.x + jobs: + format: + name: 'Format' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./uni + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v1 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + + - run: dart format . --set-exit-if-changed + lint: name: 'Lint' runs-on: ubuntu-latest + needs: format defaults: run: working-directory: ./uni @@ -11,10 +30,10 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v1 with: - java-version: '11.x' + java-version: ${{ env.FLUTTER_VERSION }} - uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.2' + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Cache pub dependencies uses: actions/cache@v2 @@ -23,12 +42,12 @@ jobs: key: ${{ runner.os }}-pub-${{ github.ref }}-${{ hashFiles('**/pubspec.lock') }} restore-keys: ${{ runner.os }}-pub-${{ github.ref }}- - - run: flutter pub get - - run: flutter analyze --no-pub --preamble . + - run: flutter analyze . test: name: 'Test' runs-on: ubuntu-latest + needs: lint defaults: run: working-directory: ./uni @@ -36,10 +55,9 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v1 with: - java-version: '11.x' + java-version: ${{ env.FLUTTER_VERSION }} - uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.2' + flutter-version: ${{ env.FLUTTER_VERSION }} - - run: flutter pub get - run: flutter test --no-sound-null-safety From 86a677e6333f6a071c319735c7134385c3eca174 Mon Sep 17 00:00:00 2001 From: Sirze01 Date: Thu, 20 Jul 2023 20:32:37 +0000 Subject: [PATCH 074/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index dee68838b..408a7d5bb 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.35+153 \ No newline at end of file +1.5.36+154 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 64863aaf9..9d03d214a 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.35+153 +version: 1.5.36+154 environment: sdk: ">=2.17.1 <3.0.0" From 37f40e674e92f9a7ce78256cb0bbc499407c799c Mon Sep 17 00:00:00 2001 From: thePeras Date: Thu, 20 Jul 2023 21:01:10 +0100 Subject: [PATCH 075/100] Formated project code --- .github/workflows/format_lint_test.yaml | 4 +- .../notifications/tuition_notification.dart | 2 +- .../fetchers/reference_fetcher.dart | 8 +- .../app_references_database.dart | 17 +- .../app_restaurant_database.dart | 5 +- .../local_storage/app_user_database.dart | 4 +- .../notification_timeout_storage.dart | 49 +++--- .../parsers/parser_course_unit_info.dart | 7 +- uni/lib/controller/parsers/parser_fees.dart | 4 +- .../controller/parsers/parser_references.dart | 18 +-- .../controller/parsers/parser_schedule.dart | 17 +- .../parsers/parser_schedule_html.dart | 25 +-- .../controller/parsers/parser_session.dart | 4 +- uni/lib/model/entities/bus_stop.dart | 5 +- uni/lib/model/entities/lecture.dart | 41 ++--- uni/lib/model/entities/reference.dart | 6 +- uni/lib/model/entities/time_utilities.dart | 14 +- .../providers/lazy/reference_provider.dart | 6 +- uni/lib/model/request_status.dart | 2 +- uni/lib/utils/duration_string_formatter.dart | 49 +++--- .../bus_stop_next_arrivals.dart | 35 ++-- .../common_widgets/expanded_image_label.dart | 22 ++- .../generic_expansion_card.dart | 27 +--- uni/lib/view/exams/exams.dart | 18 ++- uni/lib/view/exams/widgets/exam_time.dart | 3 +- .../view/locations/widgets/faculty_map.dart | 4 +- .../widgets/floorless_marker_popup.dart | 19 +-- uni/lib/view/locations/widgets/marker.dart | 5 +- .../view/locations/widgets/marker_popup.dart | 23 +-- .../profile/widgets/account_info_card.dart | 151 ++++++++---------- .../profile/widgets/course_info_card.dart | 47 ++---- .../profile/widgets/reference_section.dart | 50 +++--- .../restaurant/widgets/restaurant_slot.dart | 12 +- uni/lib/view/schedule/schedule.dart | 26 ++- .../view/schedule/widgets/schedule_slot.dart | 6 +- uni/lib/view/theme.dart | 21 ++- 36 files changed, 357 insertions(+), 399 deletions(-) diff --git a/.github/workflows/format_lint_test.yaml b/.github/workflows/format_lint_test.yaml index b6377416b..fb24e2ebc 100644 --- a/.github/workflows/format_lint_test.yaml +++ b/.github/workflows/format_lint_test.yaml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v1 with: - java-version: ${{ env.FLUTTER_VERSION }} + java-version: ${{ env.JAVA_VERSION }} - uses: subosito/flutter-action@v1 with: flutter-version: ${{ env.FLUTTER_VERSION }} @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v1 with: - java-version: ${{ env.FLUTTER_VERSION }} + java-version: ${{ env.JAVA_VERSION }} - uses: subosito/flutter-action@v1 with: flutter-version: ${{ env.FLUTTER_VERSION }} diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 9b8c3f5a9..db6e43abd 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -48,7 +48,7 @@ class TuitionNotification extends Notification { final DateTime? dueDate = await parseFeesNextLimit( await feesFetcher.getUserFeesResponse(session)); - if(dueDate == null) return false; + if (dueDate == null) return false; _dueDate = dueDate; return DateTime.now().difference(_dueDate).inDays >= -3; diff --git a/uni/lib/controller/fetchers/reference_fetcher.dart b/uni/lib/controller/fetchers/reference_fetcher.dart index 8e54ae85d..20bc965a9 100644 --- a/uni/lib/controller/fetchers/reference_fetcher.dart +++ b/uni/lib/controller/fetchers/reference_fetcher.dart @@ -4,11 +4,11 @@ import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/session.dart'; class ReferenceFetcher implements SessionDependantFetcher { - @override List getEndpoints(Session session) { - final List baseUrls = NetworkRouter.getBaseUrlsFromSession(session) - + [NetworkRouter.getBaseUrl('sasup')]; + final List baseUrls = + NetworkRouter.getBaseUrlsFromSession(session) + + [NetworkRouter.getBaseUrl('sasup')]; final List urls = baseUrls .map((url) => '${url}gpag_ccorrente_geral.conta_corrente_view') .toList(); @@ -21,4 +21,4 @@ class ReferenceFetcher implements SessionDependantFetcher { final Map query = {'pct_cod': session.studentNumber}; return NetworkRouter.getWithCookies(url, query, session); } -} \ No newline at end of file +} diff --git a/uni/lib/controller/local_storage/app_references_database.dart b/uni/lib/controller/local_storage/app_references_database.dart index d73b0d3c5..daf8682bf 100644 --- a/uni/lib/controller/local_storage/app_references_database.dart +++ b/uni/lib/controller/local_storage/app_references_database.dart @@ -10,11 +10,11 @@ import 'package:uni/model/entities/reference.dart'; /// See the [Reference] class to see what data is stored in this database. class AppReferencesDatabase extends AppDatabase { static const String createScript = - '''CREATE TABLE refs(description TEXT, entity INTEGER, ''' - '''reference INTEGER, amount REAL, limitDate TEXT)'''; + '''CREATE TABLE refs(description TEXT, entity INTEGER, ''' + '''reference INTEGER, amount REAL, limitDate TEXT)'''; - AppReferencesDatabase() : - super('refs.db', [createScript], onUpgrade: migrate, version: 2); + AppReferencesDatabase() + : super('refs.db', [createScript], onUpgrade: migrate, version: 2); /// Replaces all of the data in this database with the data from [references]. Future saveNewReferences(List references) async { @@ -48,11 +48,8 @@ class AppReferencesDatabase extends AppDatabase { /// If a row with the same data is present, it will be replaced. Future insertReferences(List references) async { for (Reference reference in references) { - await insertInDatabase( - 'refs', - reference.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace - ); + await insertInDatabase('refs', reference.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace); } } @@ -67,4 +64,4 @@ class AppReferencesDatabase extends AppDatabase { batch.execute(createScript); batch.commit(); } -} \ No newline at end of file +} diff --git a/uni/lib/controller/local_storage/app_restaurant_database.dart b/uni/lib/controller/local_storage/app_restaurant_database.dart index cbc636dc9..4c0892bc3 100644 --- a/uni/lib/controller/local_storage/app_restaurant_database.dart +++ b/uni/lib/controller/local_storage/app_restaurant_database.dart @@ -117,12 +117,13 @@ List filterPastMeals(List restaurants) { // (To replicate sigarra's behaviour for the GSheets meals) final DateTime now = DateTime.now().toUtc(); final DateTime today = DateTime.utc(now.year, now.month, now.day); - final DateTime nextSunday = today.add(Duration(days: DateTime.sunday - now.weekday)); + final DateTime nextSunday = + today.add(Duration(days: DateTime.sunday - now.weekday)); for (var restaurant in restaurantsCopy) { for (var meals in restaurant.meals.values) { meals.removeWhere( - (meal) => meal.date.isBefore(today) || meal.date.isAfter(nextSunday)); + (meal) => meal.date.isBefore(today) || meal.date.isAfter(nextSunday)); } } diff --git a/uni/lib/controller/local_storage/app_user_database.dart b/uni/lib/controller/local_storage/app_user_database.dart index 623589104..b734359ca 100644 --- a/uni/lib/controller/local_storage/app_user_database.dart +++ b/uni/lib/controller/local_storage/app_user_database.dart @@ -38,7 +38,9 @@ class AppUserDataDatabase extends AppDatabase { if (entry['key'] == 'email') email = entry['value']; if (entry['key'] == 'printBalance') printBalance = entry['value']; if (entry['key'] == 'feesBalance') feesBalance = entry['value']; - if (entry['key'] == 'feesLimit') feesLimit = DateTime.tryParse(entry['value']); + if (entry['key'] == 'feesLimit') { + feesLimit = DateTime.tryParse(entry['value']); + } } return Profile( diff --git a/uni/lib/controller/local_storage/notification_timeout_storage.dart b/uni/lib/controller/local_storage/notification_timeout_storage.dart index 4f9173d8e..6875a5293 100644 --- a/uni/lib/controller/local_storage/notification_timeout_storage.dart +++ b/uni/lib/controller/local_storage/notification_timeout_storage.dart @@ -2,59 +2,56 @@ import 'dart:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; -class NotificationTimeoutStorage{ - +class NotificationTimeoutStorage { late Map _fileContent; NotificationTimeoutStorage._create(); - Future _asyncInit() async{ + Future _asyncInit() async { _fileContent = _readContentsFile(await _getTimeoutFile()); } - static Future create() async{ + static Future create() async { final notificationStorage = NotificationTimeoutStorage._create(); await notificationStorage._asyncInit(); return notificationStorage; - } - Map _readContentsFile(File file){ - try{ + Map _readContentsFile(File file) { + try { return jsonDecode(file.readAsStringSync()); - - } on FormatException catch(_){ - return {}; + } on FormatException catch (_) { + return {}; } - } - DateTime getLastTimeNotificationExecuted(String uniqueID){ - if(!_fileContent.containsKey(uniqueID)){ - return DateTime.fromMicrosecondsSinceEpoch(0); //get 1970 to always trigger notification + DateTime getLastTimeNotificationExecuted(String uniqueID) { + if (!_fileContent.containsKey(uniqueID)) { + return DateTime.fromMicrosecondsSinceEpoch( + 0); //get 1970 to always trigger notification } return DateTime.parse(_fileContent[uniqueID]); } - Future addLastTimeNotificationExecuted(String uniqueID, DateTime lastRan) async{ + Future addLastTimeNotificationExecuted( + String uniqueID, DateTime lastRan) async { _fileContent[uniqueID] = lastRan.toIso8601String(); await _writeToFile(await _getTimeoutFile()); } - Future _writeToFile(File file) async{ + Future _writeToFile(File file) async { await file.writeAsString(jsonEncode(_fileContent)); - } - - Future _getTimeoutFile() async{ - final applicationDirectory = (await getApplicationDocumentsDirectory()).path; - if(! (await File("$applicationDirectory/notificationTimeout.json").exists())){ - //empty json - await File("$applicationDirectory/notificationTimeout.json").writeAsString("{}"); + Future _getTimeoutFile() async { + final applicationDirectory = + (await getApplicationDocumentsDirectory()).path; + if (!(await File("$applicationDirectory/notificationTimeout.json") + .exists())) { + //empty json + await File("$applicationDirectory/notificationTimeout.json") + .writeAsString("{}"); } return File("$applicationDirectory/notificationTimeout.json"); } - - -} \ No newline at end of file +} diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 78005b951..483242d1b 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -19,8 +19,8 @@ Future parseCourseUnitSheet(http.Response response) async { return CourseUnitSheet(sections); } -List parseCourseUnitClasses(http.Response response, - String baseUrl) { +List parseCourseUnitClasses( + http.Response response, String baseUrl) { final List classes = []; final document = parse(response.body); final titles = document.querySelectorAll('#conteudoinner h3').sublist(1); @@ -41,8 +41,7 @@ List parseCourseUnitClasses(http.Response response, for (final row in studentRows) { final columns = row.querySelectorAll('td.k.t'); final String studentName = columns[0].children[0].innerHtml; - final int studentNumber = - int.tryParse(columns[1].innerHtml.trim()) ?? 0; + final int studentNumber = int.tryParse(columns[1].innerHtml.trim()) ?? 0; final String studentMail = columns[2].innerHtml; final Uri studentPhoto = Uri.parse( diff --git a/uni/lib/controller/parsers/parser_fees.dart b/uni/lib/controller/parsers/parser_fees.dart index 20c7a4a55..239f5bdf2 100644 --- a/uni/lib/controller/parsers/parser_fees.dart +++ b/uni/lib/controller/parsers/parser_fees.dart @@ -27,7 +27,7 @@ Future parseFeesNextLimit(http.Response response) async { } final String limit = lines[1].querySelectorAll('.data')[1].text; - //it's completly fine to throw an exeception if it fails, in this case, + //it's completly fine to throw an exeception if it fails, in this case, //since probably sigarra is returning something we don't except - return DateTime.parse(limit); + return DateTime.parse(limit); } diff --git a/uni/lib/controller/parsers/parser_references.dart b/uni/lib/controller/parsers/parser_references.dart index 2782a1f12..d539b3a80 100644 --- a/uni/lib/controller/parsers/parser_references.dart +++ b/uni/lib/controller/parsers/parser_references.dart @@ -3,31 +3,29 @@ import 'package:html/parser.dart' show parse; import 'package:http/http.dart' as http; import 'package:uni/model/entities/reference.dart'; - /// Extracts a list of references from an HTTP [response]. Future> parseReferences(http.Response response) async { final document = parse(response.body); final List references = []; - final List rows = document - .querySelectorAll('div#tab0 > table.dadossz > tbody > tr'); + final List rows = + document.querySelectorAll('div#tab0 > table.dadossz > tbody > tr'); if (rows.length > 1) { - rows.sublist(1) - .forEach((Element tr) { + rows.sublist(1).forEach((Element tr) { final List info = tr.querySelectorAll('td'); final String description = info[0].text; final DateTime limitDate = DateTime.parse(info[2].text); final int entity = int.parse(info[3].text); final int reference = int.parse(info[4].text); - final String formattedAmount = info[5].text - .replaceFirst(',', '.') - .replaceFirst('€', ''); + final String formattedAmount = + info[5].text.replaceFirst(',', '.').replaceFirst('€', ''); final double amount = double.parse(formattedAmount); - references.add(Reference(description, limitDate, entity, reference, amount)); + references + .add(Reference(description, limitDate, entity, reference, amount)); }); } return references; -} \ No newline at end of file +} diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 4d83e9bb9..6fd72a55e 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -21,7 +21,6 @@ Future> parseSchedule(http.Response response) async { final json = jsonDecode(response.body); - final schedule = json['horario']; for (var lecture in schedule) { @@ -37,12 +36,18 @@ Future> parseSchedule(http.Response response) async { final int occurrId = lecture['ocorrencia_id']; final DateTime monday = DateTime.now().getClosestMonday(); - - final Lecture lec = Lecture.fromApi(subject, typeClass, monday.add(Duration(days:day, seconds: secBegin)), blocks, - room, teacher, classNumber, occurrId); - - lectures.add(lec); + final Lecture lec = Lecture.fromApi( + subject, + typeClass, + monday.add(Duration(days: day, seconds: secBegin)), + blocks, + room, + teacher, + classNumber, + occurrId); + + lectures.add(lec); } final lecturesList = lectures.toList(); diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index 428bbf98b..dc2e49348 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -9,8 +9,6 @@ import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/entities/time_utilities.dart'; - - Future> getOverlappedClasses( Session session, Document document) async { final List lecturesList = []; @@ -38,12 +36,11 @@ Future> getOverlappedClasses( final String? classNumber = element.querySelector('td[headers=t6] > a')?.text; - try { final DateTime fullStartTime = monday.add(Duration( - days: day, - hours: int.parse(startTime!.substring(0, 2)), - minutes: int.parse(startTime.substring(3, 5)))); + days: day, + hours: int.parse(startTime!.substring(0, 2)), + minutes: int.parse(startTime.substring(3, 5)))); final String? link = element.querySelector('td[headers=t6] > a')?.attributes['href']; @@ -57,12 +54,19 @@ Future> getOverlappedClasses( lecturesList.add(classLectures .where((element) => - element.subject == subject && - element.startTime == fullStartTime) + element.subject == subject && element.startTime == fullStartTime) .first); } catch (e) { - final Lecture lect = Lecture.fromHtml(subject!, typeClass!, monday.add(Duration(days: day)), - startTime!, 0, room!, teacher!, classNumber!, -1); + final Lecture lect = Lecture.fromHtml( + subject!, + typeClass!, + monday.add(Duration(days: day)), + startTime!, + 0, + room!, + teacher!, + classNumber!, + -1); lecturesList.add(lect); } } @@ -82,7 +86,6 @@ Future> getScheduleFromHtml( final DateTime monday = DateTime.now().getClosestMonday(); - document.querySelectorAll('.horario > tbody > tr').forEach((Element element) { if (element.getElementsByClassName('horas').isNotEmpty) { var day = 0; diff --git a/uni/lib/controller/parsers/parser_session.dart b/uni/lib/controller/parsers/parser_session.dart index fc132d344..731d583bc 100644 --- a/uni/lib/controller/parsers/parser_session.dart +++ b/uni/lib/controller/parsers/parser_session.dart @@ -1,8 +1,8 @@ import 'package:html/parser.dart'; -bool isPasswordExpired(String htmlBody){ +bool isPasswordExpired(String htmlBody) { final document = parse(htmlBody); final alerts = document.querySelectorAll('.aviso-invalidado'); - if(alerts.length < 2) return false; + if (alerts.length < 2) return false; return alerts[1].text.contains('A sua senha de acesso encontra-se expirada'); } diff --git a/uni/lib/model/entities/bus_stop.dart b/uni/lib/model/entities/bus_stop.dart index 7aa385691..38017bd19 100644 --- a/uni/lib/model/entities/bus_stop.dart +++ b/uni/lib/model/entities/bus_stop.dart @@ -6,5 +6,8 @@ class BusStopData { bool favorited; List trips; - BusStopData({required this.configuredBuses, this.favorited = false, this.trips = const []}); + BusStopData( + {required this.configuredBuses, + this.favorited = false, + this.trips = const []}); } diff --git a/uni/lib/model/entities/lecture.dart b/uni/lib/model/entities/lecture.dart index 166c72d80..66eddf33e 100644 --- a/uni/lib/model/entities/lecture.dart +++ b/uni/lib/model/entities/lecture.dart @@ -13,16 +13,8 @@ class Lecture { int occurrId; /// Creates an instance of the class [Lecture]. - Lecture( - this.subject, - this.typeClass, - this.startTime, - this.endTime, - this.blocks, - this.room, - this.teacher, - this.classNumber, - this.occurrId); + Lecture(this.subject, this.typeClass, this.startTime, this.endTime, + this.blocks, this.room, this.teacher, this.classNumber, this.occurrId); factory Lecture.fromApi( String subject, @@ -33,17 +25,9 @@ class Lecture { String teacher, String classNumber, int occurrId) { - final endTime = startTime.add(Duration(seconds:60 * 30 * blocks)); - final lecture = Lecture( - subject, - typeClass, - startTime, - endTime, - blocks, - room, - teacher, - classNumber, - occurrId); + final endTime = startTime.add(Duration(seconds: 60 * 30 * blocks)); + final lecture = Lecture(subject, typeClass, startTime, endTime, blocks, + room, teacher, classNumber, occurrId); return lecture; } @@ -66,7 +50,9 @@ class Lecture { subject, typeClass, day.add(Duration(hours: startTimeHours, minutes: startTimeMinutes)), - day.add(Duration(hours: startTimeMinutes+endTimeHours, minutes: startTimeMinutes+endTimeMinutes)), + day.add(Duration( + hours: startTimeMinutes + endTimeHours, + minutes: startTimeMinutes + endTimeMinutes)), blocks, room, teacher, @@ -76,15 +62,8 @@ class Lecture { /// Clones a lecture from the api. static Lecture clone(Lecture lec) { - return Lecture.fromApi( - lec.subject, - lec.typeClass, - lec.startTime, - lec.blocks, - lec.room, - lec.teacher, - lec.classNumber, - lec.occurrId); + return Lecture.fromApi(lec.subject, lec.typeClass, lec.startTime, + lec.blocks, lec.room, lec.teacher, lec.classNumber, lec.occurrId); } /// Clones a lecture from the html. diff --git a/uni/lib/model/entities/reference.dart b/uni/lib/model/entities/reference.dart index 156bf37fc..63a249845 100644 --- a/uni/lib/model/entities/reference.dart +++ b/uni/lib/model/entities/reference.dart @@ -5,8 +5,8 @@ class Reference { final int reference; final double amount; - Reference(this.description, this.limitDate, - this.entity, this.reference, this.amount); + Reference(this.description, this.limitDate, this.entity, this.reference, + this.amount); /// Converts this reference to a map. Map toMap() { @@ -18,4 +18,4 @@ class Reference { 'amount': amount, }; } -} \ No newline at end of file +} diff --git a/uni/lib/model/entities/time_utilities.dart b/uni/lib/model/entities/time_utilities.dart index d1d512d5a..a34dc561d 100644 --- a/uni/lib/model/entities/time_utilities.dart +++ b/uni/lib/model/entities/time_utilities.dart @@ -26,12 +26,12 @@ extension TimeString on DateTime { } } -extension ClosestMonday on DateTime{ - DateTime getClosestMonday(){ +extension ClosestMonday on DateTime { + DateTime getClosestMonday() { final DateTime day = DateUtils.dateOnly(this); - if(day.weekday >=1 && day.weekday <= 5){ - return day.subtract(Duration(days: day.weekday-1)); - } - return day.add(Duration(days: DateTime.daysPerWeek - day.weekday+1)); + if (day.weekday >= 1 && day.weekday <= 5) { + return day.subtract(Duration(days: day.weekday - 1)); + } + return day.add(Duration(days: DateTime.daysPerWeek - day.weekday + 1)); } -} \ No newline at end of file +} diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart index 63ec9b602..aca729373 100644 --- a/uni/lib/model/providers/lazy/reference_provider.dart +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -33,11 +33,11 @@ class ReferenceProvider extends StateProviderNotifier { await fetchUserReferences(referencesAction, session); } - Future fetchUserReferences(Completer action, - Session session) async { + Future fetchUserReferences( + Completer action, Session session) async { try { final response = - await ReferenceFetcher().getUserReferenceResponse(session); + await ReferenceFetcher().getUserReferenceResponse(session); final List references = await parseReferences(response); updateStatus(RequestStatus.successful); diff --git a/uni/lib/model/request_status.dart b/uni/lib/model/request_status.dart index 3fcc52a83..c44f0c47e 100644 --- a/uni/lib/model/request_status.dart +++ b/uni/lib/model/request_status.dart @@ -1 +1 @@ -enum RequestStatus { none, busy, failed, successful } \ No newline at end of file +enum RequestStatus { none, busy, failed, successful } diff --git a/uni/lib/utils/duration_string_formatter.dart b/uni/lib/utils/duration_string_formatter.dart index 91eef0fa7..673084283 100644 --- a/uni/lib/utils/duration_string_formatter.dart +++ b/uni/lib/utils/duration_string_formatter.dart @@ -1,46 +1,49 @@ -extension DurationStringFormatter on Duration{ - +extension DurationStringFormatter on Duration { static final formattingRegExp = RegExp('{}'); - String toFormattedString(String singularPhrase, String pluralPhrase, {String term = "{}"}){ + String toFormattedString(String singularPhrase, String pluralPhrase, + {String term = "{}"}) { if (!singularPhrase.contains(term) || !pluralPhrase.contains(term)) { - throw ArgumentError("singularPhrase or plurarPhrase don't have a string that can be formatted..."); + throw ArgumentError( + "singularPhrase or plurarPhrase don't have a string that can be formatted..."); } - if(inSeconds == 1){ + if (inSeconds == 1) { return singularPhrase.replaceAll(formattingRegExp, "$inSeconds segundo"); } - if(inSeconds < 60){ + if (inSeconds < 60) { return pluralPhrase.replaceAll(formattingRegExp, "$inSeconds segundos"); } - if(inMinutes == 1){ + if (inMinutes == 1) { return singularPhrase.replaceAll(formattingRegExp, "$inMinutes minuto"); } - if(inMinutes < 60){ + if (inMinutes < 60) { return pluralPhrase.replaceAll(formattingRegExp, "$inMinutes minutos"); } - if(inHours == 1){ + if (inHours == 1) { return singularPhrase.replaceAll(formattingRegExp, "$inHours hora"); } - if(inHours < 24){ + if (inHours < 24) { return pluralPhrase.replaceAll(formattingRegExp, "$inHours horas"); } - if(inDays == 1){ + if (inDays == 1) { return singularPhrase.replaceAll(formattingRegExp, "$inDays dia"); } - if(inDays <= 7){ + if (inDays <= 7) { return pluralPhrase.replaceAll(formattingRegExp, "$inDays dias"); - } - if((inDays / 7).floor() == 1){ - return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semana"); + if ((inDays / 7).floor() == 1) { + return singularPhrase.replaceAll( + formattingRegExp, "${(inDays / 7).floor()} semana"); } - if((inDays / 7).floor() > 1){ - return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 7).floor()} semanas"); - } - if((inDays / 30).floor() == 1){ - return singularPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} mês"); + if ((inDays / 7).floor() > 1) { + return pluralPhrase.replaceAll( + formattingRegExp, "${(inDays / 7).floor()} semanas"); } - return pluralPhrase.replaceAll(formattingRegExp, "${(inDays / 30).floor()} meses"); - + if ((inDays / 30).floor() == 1) { + return singularPhrase.replaceAll( + formattingRegExp, "${(inDays / 30).floor()} mês"); + } + return pluralPhrase.replaceAll( + formattingRegExp, "${(inDays / 30).floor()} meses"); } -} \ No newline at end of file +} diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 2d1ff3a40..56688a6f8 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -25,8 +25,8 @@ class BusStopNextArrivalsPageState Widget getBody(BuildContext context) { return LazyConsumer( builder: (context, busProvider) => ListView(children: [ - NextArrivals(busProvider.configuredBusStops, busProvider.status) - ])); + NextArrivals(busProvider.configuredBusStops, busProvider.status) + ])); } @override @@ -84,19 +84,22 @@ class NextArrivalsState extends State { if (widget.buses.isNotEmpty) { result.addAll(getContent(context)); } else { - result.add( - ImageLabel(imagePath: 'assets/images/bus.png', label: 'Não percas nenhum autocarro', labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Theme.of(context).colorScheme.primary)) - ); - result.add( - Column( - children: [ - ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const BusStopSelectionPage())), - child: const Text('Adicionar'), - ), - ])); + result.add(ImageLabel( + imagePath: 'assets/images/bus.png', + label: 'Não percas nenhum autocarro', + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 17, + color: Theme.of(context).colorScheme.primary))); + result.add(Column(children: [ + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage())), + child: const Text('Adicionar'), + ), + ])); } return result; @@ -219,4 +222,4 @@ class NextArrivalsState extends State { return rows; } -} \ No newline at end of file +} diff --git a/uni/lib/view/common_widgets/expanded_image_label.dart b/uni/lib/view/common_widgets/expanded_image_label.dart index f87a1121c..f4a60e0d0 100644 --- a/uni/lib/view/common_widgets/expanded_image_label.dart +++ b/uni/lib/view/common_widgets/expanded_image_label.dart @@ -7,7 +7,14 @@ class ImageLabel extends StatelessWidget { final String sublabel; final TextStyle? sublabelTextStyle; - const ImageLabel({Key? key, required this.imagePath, required this.label, this.labelTextStyle, this.sublabel = '', this.sublabelTextStyle}) : super(key: key); + const ImageLabel( + {Key? key, + required this.imagePath, + required this.label, + this.labelTextStyle, + this.sublabel = '', + this.sublabelTextStyle}) + : super(key: key); @override Widget build(BuildContext context) { @@ -24,13 +31,12 @@ class ImageLabel extends StatelessWidget { label, style: labelTextStyle, ), - if(sublabel.isNotEmpty) - const SizedBox(height: 20), - Text( - sublabel, - style: sublabelTextStyle, - ), + if (sublabel.isNotEmpty) const SizedBox(height: 20), + Text( + sublabel, + style: sublabelTextStyle, + ), ], ); } -} \ No newline at end of file +} diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index eac8afca2..d26f3a631 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -10,14 +10,10 @@ abstract class GenericExpansionCard extends StatelessWidget { {Key? key, this.smallTitle = false, this.cardMargin}) : super(key: key); - TextStyle? getTitleStyle(BuildContext context) => - Theme - .of(context) - .textTheme - .headlineSmall - ?.apply(color: Theme - .of(context) - .primaryColor); + TextStyle? getTitleStyle(BuildContext context) => Theme.of(context) + .textTheme + .headlineSmall + ?.apply(color: Theme.of(context).primaryColor); String getTitle(); @@ -28,24 +24,17 @@ abstract class GenericExpansionCard extends StatelessWidget { return Container( margin: cardMargin ?? const EdgeInsets.fromLTRB(20, 10, 20, 0), child: ExpansionTileCard( - expandedTextColor: Theme - .of(context) - .primaryColor, + expandedTextColor: Theme.of(context).primaryColor, heightFactorCurve: Curves.ease, turnsCurve: Curves.easeOutBack, - expandedColor: (Theme - .of(context) - .brightness == Brightness.light) + expandedColor: (Theme.of(context).brightness == Brightness.light) ? const Color.fromARGB(0xf, 0, 0, 0) : const Color.fromARGB(255, 43, 43, 43), title: Text(getTitle(), - style: Theme - .of(context) + style: Theme.of(context) .textTheme .headlineSmall - ?.apply(color: Theme - .of(context) - .primaryColor)), + ?.apply(color: Theme.of(context).primaryColor)), elevation: 0, children: [ Container( diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index a8b386f82..1527e3b6e 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -29,7 +29,7 @@ class ExamsPageViewState extends GeneralPageViewState { Column( mainAxisSize: MainAxisSize.max, children: - createExamsColumn(context, examProvider.getFilteredExams()), + createExamsColumn(context, examProvider.getFilteredExams()), ) ], ); @@ -45,14 +45,16 @@ class ExamsPageViewState extends GeneralPageViewState { if (exams.isEmpty) { columns.add(Center( heightFactor: 1.2, - child: ImageLabel(imagePath: 'assets/images/vacation.png', + child: ImageLabel( + imagePath: 'assets/images/vacation.png', label: 'Parece que estás de férias!', - labelTextStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Theme.of(context).colorScheme.primary), + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary), sublabel: 'Não tens exames marcados', sublabelTextStyle: const TextStyle(fontSize: 15), - ) - ) - ); + ))); return columns; } @@ -114,7 +116,7 @@ class ExamsPageViewState extends GeneralPageViewState { Widget createExamContext(context, Exam exam) { final isHidden = - Provider.of(context).hiddenExams.contains(exam.id); + Provider.of(context).hiddenExams.contains(exam.id); return Container( key: Key('$exam-exam'), margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), @@ -130,4 +132,4 @@ class ExamsPageViewState extends GeneralPageViewState { return Provider.of(context, listen: false) .forceRefresh(context); } -} \ No newline at end of file +} diff --git a/uni/lib/view/exams/widgets/exam_time.dart b/uni/lib/view/exams/widgets/exam_time.dart index 1c0615690..884631aa4 100644 --- a/uni/lib/view/exams/widgets/exam_time.dart +++ b/uni/lib/view/exams/widgets/exam_time.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class ExamTime extends StatelessWidget { final String begin; - const ExamTime({Key? key, required this.begin}) - : super(key: key); + const ExamTime({Key? key, required this.begin}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/uni/lib/view/locations/widgets/faculty_map.dart b/uni/lib/view/locations/widgets/faculty_map.dart index 29e8c3bdb..b0398de0b 100644 --- a/uni/lib/view/locations/widgets/faculty_map.dart +++ b/uni/lib/view/locations/widgets/faculty_map.dart @@ -3,7 +3,6 @@ import 'package:latlong2/latlong.dart'; import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/map.dart'; - class FacultyMap extends StatelessWidget { final String faculty; final List locations; @@ -22,7 +21,7 @@ class FacultyMap extends StatelessWidget { locations: locations, ); default: - return Container(); // Should not happen + return Container(); // Should not happen } } @@ -32,4 +31,3 @@ class FacultyMap extends StatelessWidget { : Theme.of(context).colorScheme.tertiary; } } - diff --git a/uni/lib/view/locations/widgets/floorless_marker_popup.dart b/uni/lib/view/locations/widgets/floorless_marker_popup.dart index e7b3743a7..cc156f16a 100644 --- a/uni/lib/view/locations/widgets/floorless_marker_popup.dart +++ b/uni/lib/view/locations/widgets/floorless_marker_popup.dart @@ -26,8 +26,9 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { spacing: 8, children: (showId ? [Text(locationGroup.id.toString())] - : []) - + locations.map((location) => LocationRow(location: location)) + : []) + + locations + .map((location) => LocationRow(location: location)) .toList(), )), ); @@ -36,13 +37,13 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { List buildLocations(BuildContext context, List locations) { return locations .map((location) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(location.description(), - textAlign: TextAlign.left, - style: TextStyle(color: FacultyMap.getFontColor(context))) - ], - )) + mainAxisSize: MainAxisSize.min, + children: [ + Text(location.description(), + textAlign: TextAlign.left, + style: TextStyle(color: FacultyMap.getFontColor(context))) + ], + )) .toList(); } } diff --git a/uni/lib/view/locations/widgets/marker.dart b/uni/lib/view/locations/widgets/marker.dart index 13bda1c9e..626f57dff 100644 --- a/uni/lib/view/locations/widgets/marker.dart +++ b/uni/lib/view/locations/widgets/marker.dart @@ -22,9 +22,8 @@ class LocationMarker extends Marker { color: Theme.of(ctx).colorScheme.primary, ), borderRadius: const BorderRadius.all(Radius.circular(20))), - child: MarkerIcon( - location: locationGroup.getLocationWithMostWeight() - ), + child: + MarkerIcon(location: locationGroup.getLocationWithMostWeight()), ), ); } diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 7f6ed3dda..7802b1f97 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -25,16 +25,17 @@ class LocationMarkerPopup extends StatelessWidget { children: (showId ? [Text(locationGroup.id.toString())] : []) + - getEntries().map((entry) => - Floor(floor: entry.key, locations: entry.value) - ).toList(), + getEntries() + .map((entry) => + Floor(floor: entry.key, locations: entry.value)) + .toList(), )), ); } List>> getEntries() { final List>> entries = - locationGroup.floors.entries.toList(); + locationGroup.floors.entries.toList(); entries.sort((current, next) => -current.key.compareTo(next.key)); return entries; } @@ -52,9 +53,9 @@ class Floor extends StatelessWidget { final Color fontColor = FacultyMap.getFontColor(context); final String floorString = - 0 <= floor && floor <= 9 //To maintain layout of popup - ? ' $floor' - : '$floor'; + 0 <= floor && floor <= 9 //To maintain layout of popup + ? ' $floor' + : '$floor'; final Widget floorCol = Column( mainAxisSize: MainAxisSize.min, @@ -62,13 +63,13 @@ class Floor extends StatelessWidget { Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), child: - Text('Andar $floorString', style: TextStyle(color: fontColor))) + Text('Andar $floorString', style: TextStyle(color: fontColor))) ], ); final Widget locationsColumn = Container( padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), decoration: - BoxDecoration(border: Border(left: BorderSide(color: fontColor))), + BoxDecoration(border: Border(left: BorderSide(color: fontColor))), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -84,10 +85,10 @@ class Floor extends StatelessWidget { class LocationRow extends StatelessWidget { final Location location; final Color color; - + const LocationRow({Key? key, required this.location, required this.color}) : super(key: key); - + @override Widget build(BuildContext context) { return Row( diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 703a3d45d..5e262ec20 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -14,8 +14,8 @@ import 'package:uni/view/profile/widgets/tuition_notification_switch.dart'; class AccountInfoCard extends GenericCard { AccountInfoCard({Key? key}) : super(key: key); - const AccountInfoCard.fromEditingInformation(Key key, bool editingMode, - Function()? onDelete) + const AccountInfoCard.fromEditingInformation( + Key key, bool editingMode, Function()? onDelete) : super.fromEditingInformation(key, editingMode, onDelete); @override @@ -29,88 +29,70 @@ class AccountInfoCard extends GenericCard { Widget buildCardContent(BuildContext context) { return LazyConsumer( builder: (context, profileStateProvider) { - return LazyConsumer( - builder: (context, referenceProvider) { - final profile = profileStateProvider.profile; - final List references = referenceProvider.references; + return LazyConsumer( + builder: (context, referenceProvider) { + final profile = profileStateProvider.profile; + final List references = referenceProvider.references; - return Column(children: [ - Table( - columnWidths: const {1: FractionColumnWidth(.4)}, - defaultVerticalAlignment: TableCellVerticalAlignment - .middle, - children: [ - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 20.0, bottom: 8.0, left: 20.0), - child: Text('Saldo: ', - style: Theme - .of(context) - .textTheme - .titleSmall), - ), - Container( - margin: const EdgeInsets.only( - top: 20.0, bottom: 8.0, right: 30.0), - child: getInfoText(profile.feesBalance, context)) - ]), - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: Text('Data limite próxima prestação: ', - style: Theme - .of(context) - .textTheme - .titleSmall), - ), - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, right: 30.0), - child: getInfoText( - profile.feesLimit != null - ? DateFormat('yyyy-MM-dd') - .format(profile.feesLimit!) - : 'Sem data', - context)) - ]), - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: Text("Notificar próxima data limite: ", - style: Theme - .of(context) - .textTheme - .titleSmall)), - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: const TuitionNotificationSwitch()) - ]) - ]), + return Column(children: [ + Table( + columnWidths: const {1: FractionColumnWidth(.4)}, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow(children: [ Container( - padding: const EdgeInsets.all(10), - child: Row(children: [ - Text('Referências pendentes', - style: Theme - .of(context) - .textTheme - .titleLarge - ?.apply( - color: Theme - .of(context) - .colorScheme - .secondary)), - ])), - ReferenceList(references: references), - const SizedBox(height: 10), - showLastRefreshedTime( - profileStateProvider.feesRefreshTime, context) - ]); - }); - }); + margin: const EdgeInsets.only( + top: 20.0, bottom: 8.0, left: 20.0), + child: Text('Saldo: ', + style: Theme.of(context).textTheme.titleSmall), + ), + Container( + margin: const EdgeInsets.only( + top: 20.0, bottom: 8.0, right: 30.0), + child: getInfoText(profile.feesBalance, context)) + ]), + TableRow(children: [ + Container( + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: Text('Data limite próxima prestação: ', + style: Theme.of(context).textTheme.titleSmall), + ), + Container( + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, right: 30.0), + child: getInfoText( + profile.feesLimit != null + ? DateFormat('yyyy-MM-dd') + .format(profile.feesLimit!) + : 'Sem data', + context)) + ]), + TableRow(children: [ + Container( + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: Text("Notificar próxima data limite: ", + style: Theme.of(context).textTheme.titleSmall)), + Container( + margin: const EdgeInsets.only( + top: 8.0, bottom: 20.0, left: 20.0), + child: const TuitionNotificationSwitch()) + ]) + ]), + Container( + padding: const EdgeInsets.all(10), + child: Row(children: [ + Text('Referências pendentes', + style: Theme.of(context).textTheme.titleLarge?.apply( + color: Theme.of(context).colorScheme.secondary)), + ])), + ReferenceList(references: references), + const SizedBox(height: 10), + showLastRefreshedTime(profileStateProvider.feesRefreshTime, context) + ]); + }); + }); } @override @@ -132,10 +114,7 @@ class ReferenceList extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10), child: Text( "Não existem referências a pagar", - style: Theme - .of(context) - .textTheme - .titleSmall, + style: Theme.of(context).textTheme.titleSmall, textScaleFactor: 0.96, ), ); diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index f328fbc25..8b7eeeb27 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -18,14 +18,11 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 20.0), child: Text('Ano curricular atual: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 20.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 20.0, bottom: 8.0, right: 20.0), child: getInfoText(course.currYear ?? 'Indisponível', context), ) ]), @@ -33,14 +30,11 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Estado atual: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText(course.state ?? 'Indisponível', context), ) ]), @@ -48,18 +42,14 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Ano da primeira inscrição: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.firstEnrollment != null - ? '${course.firstEnrollment}/${course.firstEnrollment! + - 1}' + ? '${course.firstEnrollment}/${course.firstEnrollment! + 1}' : '?', context)) ]), @@ -67,14 +57,11 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Faculdade: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.faculty?.toUpperCase() ?? 'Indisponível', context)) ]), @@ -82,14 +69,11 @@ class CourseInfoCard extends GenericCard { Container( margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), child: Text('Média: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), child: getInfoText( course.currentAverage?.toString() ?? 'Indisponível', context)) @@ -97,16 +81,13 @@ class CourseInfoCard extends GenericCard { TableRow(children: [ Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), child: Text('ECTs realizados: ', - style: Theme - .of(context) - .textTheme - .titleSmall), + style: Theme.of(context).textTheme.titleSmall), ), Container( margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, right: 20.0), + const EdgeInsets.only(top: 10.0, bottom: 20.0, right: 20.0), child: getInfoText( course.finishedEcts?.toString().replaceFirst('.0', '') ?? '?', diff --git a/uni/lib/view/profile/widgets/reference_section.dart b/uni/lib/view/profile/widgets/reference_section.dart index 938c86468..d32628460 100644 --- a/uni/lib/view/profile/widgets/reference_section.dart +++ b/uni/lib/view/profile/widgets/reference_section.dart @@ -4,7 +4,6 @@ import 'package:intl/intl.dart'; import 'package:uni/model/entities/reference.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; - class ReferenceSection extends StatelessWidget { final Reference reference; @@ -12,17 +11,22 @@ class ReferenceSection extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - TitleText(title: reference.description), - InfoCopyRow(infoName: 'Entidade', info: reference.entity.toString(), - copyMessage: 'Entidade copiada!'), - InfoCopyRow(infoName: 'Referência', info: reference.reference.toString(), - copyMessage: 'Referência copiada!'), - InfoCopyRow(infoName: 'Montante', info: reference.amount.toString(), - copyMessage: 'Montante copiado!', isMoney: true), - ] - ); + return Column(children: [ + TitleText(title: reference.description), + InfoCopyRow( + infoName: 'Entidade', + info: reference.entity.toString(), + copyMessage: 'Entidade copiada!'), + InfoCopyRow( + infoName: 'Referência', + info: reference.reference.toString(), + copyMessage: 'Referência copiada!'), + InfoCopyRow( + infoName: 'Montante', + info: reference.amount.toString(), + copyMessage: 'Montante copiado!', + isMoney: true), + ]); } } @@ -30,17 +34,14 @@ class InfoText extends StatelessWidget { final String text; final Color? color; - const InfoText({Key? key, required this.text, this.color}) - : super(key: key); + const InfoText({Key? key, required this.text, this.color}) : super(key: key); @override Widget build(BuildContext context) { return Text( text, textScaleFactor: 0.9, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: color - ), + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: color), ); } } @@ -71,8 +72,13 @@ class InfoCopyRow extends StatelessWidget { final String copyMessage; final bool isMoney; - const InfoCopyRow({Key? key, required this.infoName, required this.info, - required this.copyMessage, this.isMoney = false}) : super(key: key); + const InfoCopyRow( + {Key? key, + required this.infoName, + required this.info, + required this.copyMessage, + this.isMoney = false}) + : super(key: key); @override Widget build(BuildContext context) { @@ -97,6 +103,6 @@ class InfoCopyRow extends StatelessWidget { ); } - String _getMoneyAmount() - => NumberFormat.simpleCurrency(locale: 'eu').format(double.parse(info)); -} \ No newline at end of file + String _getMoneyAmount() => + NumberFormat.simpleCurrency(locale: 'eu').format(double.parse(info)); +} diff --git a/uni/lib/view/restaurant/widgets/restaurant_slot.dart b/uni/lib/view/restaurant/widgets/restaurant_slot.dart index 2f56225a8..564e62ae2 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_slot.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_slot.dart @@ -50,7 +50,7 @@ class RestaurantSlotType extends StatelessWidget { 'salada': 'assets/meal-icons/salad.svg', }; - const RestaurantSlotType({Key? key, required this.type}): super(key: key); + const RestaurantSlotType({Key? key, required this.type}) : super(key: key); @override Widget build(BuildContext context) { @@ -59,11 +59,11 @@ class RestaurantSlotType extends StatelessWidget { message: type, child: icon != '' ? SvgPicture.asset( - icon, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), - height: 20, - ) + icon, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, BlendMode.srcIn), + height: 20, + ) : null); } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 04e0830fd..bb6800c51 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -45,7 +45,7 @@ class SchedulePageView extends StatefulWidget { final int weekDay = DateTime.now().weekday; static final List daysOfTheWeek = - TimeString.getWeekdaysStrings(includeWeekend: false); + TimeString.getWeekdaysStrings(includeWeekend: false); static List> groupLecturesByDay(schedule) { final aggLectures = >[]; @@ -105,10 +105,10 @@ class SchedulePageViewState extends GeneralPageViewState ), Expanded( child: TabBarView( - controller: tabController, - children: + controller: tabController, + children: createSchedule(context, widget.lectures, widget.scheduleStatus), - )) + )) ]); } @@ -169,14 +169,15 @@ class SchedulePageViewState extends GeneralPageViewState List? lectures, RequestStatus? scheduleStatus) { final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); return RequestDependentWidgetBuilder( - status: scheduleStatus ?? RequestStatus.none, - builder: () => dayColumnBuilder(day, aggLectures[day], context), - hasContentPredicate: aggLectures[day].isNotEmpty, + status: scheduleStatus ?? RequestStatus.none, + builder: () => dayColumnBuilder(day, aggLectures[day], context), + hasContentPredicate: aggLectures[day].isNotEmpty, onNullContent: Center( - child: ImageLabel(imagePath: 'assets/images/schedule.png', label: 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', labelTextStyle: const TextStyle(fontSize: 15), - ) - ) - ); + child: ImageLabel( + imagePath: 'assets/images/schedule.png', + label: 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', + labelTextStyle: const TextStyle(fontSize: 15), + ))); } @override @@ -185,6 +186,3 @@ class SchedulePageViewState extends GeneralPageViewState .forceRefresh(context); } } - - - diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index de4266557..3291400ce 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -38,7 +38,8 @@ class ScheduleSlot extends StatelessWidget { Widget createScheduleSlotRow(context) { return Container( - key: Key('schedule-slot-time-${DateFormat("HH:mm").format(begin)}-${DateFormat("HH:mm").format(end)}'), + key: Key( + 'schedule-slot-time-${DateFormat("HH:mm").format(begin)}-${DateFormat("HH:mm").format(end)}'), margin: const EdgeInsets.only(top: 3.0, bottom: 3.0), child: Row( mainAxisSize: MainAxisSize.max, @@ -50,7 +51,8 @@ class ScheduleSlot extends StatelessWidget { Widget createScheduleSlotTime(context) { return Column( - key: Key('schedule-slot-time-${DateFormat("HH:mm").format(begin)}-${DateFormat("HH:mm").format(end)}'), + key: Key( + 'schedule-slot-time-${DateFormat("HH:mm").format(begin)}-${DateFormat("HH:mm").format(end)}'), children: [ createScheduleTime(DateFormat("HH:mm").format(begin), context), createScheduleTime(DateFormat("HH:mm").format(end), context) diff --git a/uni/lib/view/theme.dart b/uni/lib/view/theme.dart index 8684afef8..985509e79 100644 --- a/uni/lib/view/theme.dart +++ b/uni/lib/view/theme.dart @@ -53,20 +53,24 @@ ThemeData applicationLightTheme = ThemeData( textTheme: _textTheme, switchTheme: SwitchThemeData( thumbColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), trackColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), ), radioTheme: RadioThemeData( fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), ), checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? darkRed : null, + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), )); @@ -97,16 +101,19 @@ ThemeData applicationDarkTheme = ThemeData( textTheme: _textTheme.apply(bodyColor: _lightGrey), switchTheme: SwitchThemeData( trackColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _lightGrey : null, + (Set states) => + states.contains(MaterialState.selected) ? _lightGrey : null, ), ), radioTheme: RadioThemeData( fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, + (Set states) => + states.contains(MaterialState.selected) ? _mildBlack : null, ), ), checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty.resolveWith( - (Set states) => states.contains(MaterialState.selected) ? _mildBlack : null, + (Set states) => + states.contains(MaterialState.selected) ? _mildBlack : null, ), )); From 43b33ce26578c378530252b7b1b8c680ba19cccf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:00:13 +0000 Subject: [PATCH 076/100] Bump flutter_map_marker_popup from 4.1.0 to 5.0.0 in /uni Bumps [flutter_map_marker_popup](https://github.com/rorystephenson/flutter_map_marker_popup) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/rorystephenson/flutter_map_marker_popup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rorystephenson/flutter_map_marker_popup/commits) --- updated-dependencies: - dependency-name: flutter_map_marker_popup dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- uni/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 9d03d214a..f6c60ffb1 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: cached_network_image: ^3.2.3 cupertino_icons: ^1.0.2 latlong2: ^0.8.1 - flutter_map_marker_popup: ^4.0.1 + flutter_map_marker_popup: ^5.0.0 workmanager: ^0.5.1 flutter_local_notifications: ^15.1.0+1 percent_indicator: ^4.2.2 From 6882eb101df58150d25351999c494e360ffddbdc Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Tue, 18 Jul 2023 16:23:56 +0100 Subject: [PATCH 077/100] Use new popup optionlass --- uni/lib/view/locations/widgets/map.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index 42c8a13d9..74df51ed3 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -64,22 +64,24 @@ class LocationsMap extends StatelessWidget { subdomains: const ['a', 'b', 'c'], tileProvider: CachedTileProvider(), ), - PopupMarkerLayerWidget( + PopupMarkerLayer( options: PopupMarkerLayerOptions( markers: locations.map((location) { return LocationMarker(location.latlng, location); }).toList(), popupController: _popupLayerController, - popupAnimation: const PopupAnimation.fade( - duration: Duration(milliseconds: 400)), - popupBuilder: (_, Marker marker) { - if (marker is LocationMarker) { - return marker.locationGroup.isFloorless - ? FloorlessLocationMarkerPopup(marker.locationGroup) - : LocationMarkerPopup(marker.locationGroup); - } - return const Card(child: Text('undefined')); - }, + popupDisplayOptions: PopupDisplayOptions( + animation: const PopupAnimation.fade( + duration: Duration(milliseconds: 400)), + builder: (_, Marker marker) { + if (marker is LocationMarker) { + return marker.locationGroup.isFloorless + ? FloorlessLocationMarkerPopup(marker.locationGroup) + : LocationMarkerPopup(marker.locationGroup); + } + return const Card(child: Text('undefined')); + }, + ), ), ), ]); From 2773ac2e3255c23f6f53a7fc534f1b013e07ea7f Mon Sep 17 00:00:00 2001 From: Sirze01 Date: Thu, 20 Jul 2023 21:46:53 +0000 Subject: [PATCH 078/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 408a7d5bb..9e098bc80 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.36+154 \ No newline at end of file +1.5.37+155 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index f6c60ffb1..7f081fa21 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.36+154 +version: 1.5.37+155 environment: sdk: ">=2.17.1 <3.0.0" From f4ac665a10372d630fbf68815de0ea8862f1b467 Mon Sep 17 00:00:00 2001 From: bdmendes Date: Thu, 20 Jul 2023 22:15:40 +0000 Subject: [PATCH 079/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 9e098bc80..180d31cef 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.37+155 \ No newline at end of file +1.5.38+156 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 7f081fa21..391ae70b1 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.37+155 +version: 1.5.38+156 environment: sdk: ">=2.17.1 <3.0.0" From bfad74452309b1d6f98a3047fa4a4b318024077b Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 12:13:39 +0100 Subject: [PATCH 080/100] Remove completers from bus stops --- .../providers/lazy/bus_stop_provider.dart | 25 +++++++------------ .../widgets/bus_stop_search.dart | 2 +- .../widgets/bus_stop_selection_row.dart | 5 ++-- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/uni/lib/model/providers/lazy/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart index 628e9dc9d..50e478295 100644 --- a/uni/lib/model/providers/lazy/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -31,14 +31,10 @@ class BusStopProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final action = Completer(); - getUserBusTrips(action); - await action.future; + await fetchUserBusTrips(); } - getUserBusTrips(Completer action) async { - updateStatus(RequestStatus.busy); - + Future fetchUserBusTrips() async { try { for (String stopCode in configuredBusStops.keys) { final List stopTrips = @@ -52,12 +48,9 @@ class BusStopProvider extends StateProviderNotifier { Logger().e('Failed to get Bus Stop information'); updateStatus(RequestStatus.failed); } - - action.complete(); } - addUserBusStop( - Completer action, String stopCode, BusStopData stopData) async { + Future addUserBusStop(String stopCode, BusStopData stopData) async { updateStatus(RequestStatus.busy); if (_configuredBusStops.containsKey(stopCode)) { @@ -69,30 +62,30 @@ class BusStopProvider extends StateProviderNotifier { _configuredBusStops[stopCode] = stopData; } - getUserBusTrips(action); + await fetchUserBusTrips(); final AppBusStopDatabase db = AppBusStopDatabase(); db.setBusStops(configuredBusStops); } - removeUserBusStop(Completer action, String stopCode) async { + Future removeUserBusStop(String stopCode) async { updateStatus(RequestStatus.busy); _configuredBusStops.remove(stopCode); notifyListeners(); - getUserBusTrips(action); + await fetchUserBusTrips(); final AppBusStopDatabase db = AppBusStopDatabase(); db.setBusStops(_configuredBusStops); } - toggleFavoriteUserBusStop( - Completer action, String stopCode, BusStopData stopData) async { + Future toggleFavoriteUserBusStop( + String stopCode, BusStopData stopData) async { _configuredBusStops[stopCode]!.favorited = !_configuredBusStops[stopCode]!.favorited; notifyListeners(); - getUserBusTrips(action); + await fetchUserBusTrips(); final AppBusStopDatabase db = AppBusStopDatabase(); db.updateFavoriteBusStop(stopCode); diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart index 86a619b67..a5f946f38 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_search.dart @@ -100,7 +100,7 @@ class BusStopSearch extends SearchDelegate { onPressed: () async { if (stopData!.configuredBuses.isNotEmpty) { Provider.of(context, listen: false) - .addUserBusStop(Completer(), stopCode!, stopData!); + .addUserBusStop(stopCode!, stopData!); Navigator.pop(context); } }) diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart index 4d09758b7..62ac1f29a 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart @@ -21,13 +21,12 @@ class BusStopSelectionRowState extends State { Future deleteStop(BuildContext context) async { Provider.of(context, listen: false) - .removeUserBusStop(Completer(), widget.stopCode); + .removeUserBusStop(widget.stopCode); } Future toggleFavorite(BuildContext context) async { Provider.of(context, listen: false) - .toggleFavoriteUserBusStop( - Completer(), widget.stopCode, widget.stopData); + .toggleFavoriteUserBusStop(widget.stopCode, widget.stopData); } @override From 4b896d2c0f9fea5e74da8d55a11888b6e8332563 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 12:31:12 +0100 Subject: [PATCH 081/100] Remove completers from untested providers --- .../providers/lazy/calendar_provider.dart | 13 ++--- .../lazy/library_occupation_provider.dart | 23 ++------- .../providers/lazy/reference_provider.dart | 17 ++----- .../providers/lazy/restaurant_provider.dart | 19 +++---- .../providers/startup/profile_provider.dart | 50 +++---------------- 5 files changed, 27 insertions(+), 95 deletions(-) diff --git a/uni/lib/model/providers/lazy/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart index 1a204c7d8..4776f6bcf 100644 --- a/uni/lib/model/providers/lazy/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/calendar_fetcher_html.dart'; import 'package:uni/controller/local_storage/app_calendar_database.dart'; import 'package:uni/model/entities/calendar_event.dart'; @@ -21,26 +20,20 @@ class CalendarProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final Completer action = Completer(); - getCalendarFromFetcher(session, action); - await action.future; + await fetchCalendar(session); } - getCalendarFromFetcher(Session session, Completer action) async { + Future fetchCalendar(Session session) async { try { - updateStatus(RequestStatus.busy); - _calendar = await CalendarFetcherHtml().getCalendar(session); - notifyListeners(); final CalendarDatabase db = CalendarDatabase(); db.saveCalendar(calendar); + updateStatus(RequestStatus.successful); } catch (e) { - Logger().e('Failed to get the Calendar: ${e.toString()}'); updateStatus(RequestStatus.failed); } - action.complete(); } @override diff --git a/uni/lib/model/providers/lazy/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart index 3b2ffed9e..26a1dbeb5 100644 --- a/uni/lib/model/providers/lazy/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/library_occupation_fetcher.dart'; import 'package:uni/controller/local_storage/app_library_occupation_database.dart'; import 'package:uni/model/entities/library_occupation.dart'; @@ -26,32 +25,20 @@ class LibraryOccupationProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final Completer action = Completer(); - getLibraryOccupation(session, action); - await action.future; + await fetchLibraryOccupation(session); } - void getLibraryOccupation( - Session session, - Completer action, - ) async { + Future fetchLibraryOccupation(Session session) async { try { - updateStatus(RequestStatus.busy); - - final LibraryOccupation occupation = - await LibraryOccupationFetcherSheets() - .getLibraryOccupationFromSheets(session); + _occupation = await LibraryOccupationFetcherSheets() + .getLibraryOccupationFromSheets(session); final LibraryOccupationDatabase db = LibraryOccupationDatabase(); - db.saveOccupation(occupation); + db.saveOccupation(_occupation!); - _occupation = occupation; - notifyListeners(); updateStatus(RequestStatus.successful); } catch (e) { - Logger().e('Failed to get Occupation: ${e.toString()}'); updateStatus(RequestStatus.failed); } - action.complete(); } } diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart index 63ec9b602..829658416 100644 --- a/uni/lib/model/providers/lazy/reference_provider.dart +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/reference_fetcher.dart'; import 'package:uni/controller/local_storage/app_references_database.dart'; import 'package:uni/controller/parsers/parser_references.dart'; @@ -29,28 +28,22 @@ class ReferenceProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final referencesAction = Completer(); - await fetchUserReferences(referencesAction, session); + await fetchUserReferences(session); } - Future fetchUserReferences(Completer action, - Session session) async { + Future fetchUserReferences(Session session) async { try { final response = - await ReferenceFetcher().getUserReferenceResponse(session); - final List references = await parseReferences(response); + await ReferenceFetcher().getUserReferenceResponse(session); + + _references = await parseReferences(response); updateStatus(RequestStatus.successful); final referencesDb = AppReferencesDatabase(); referencesDb.saveNewReferences(references); - - _references = references; } catch (e) { - Logger().e('Failed to get References info'); updateStatus(RequestStatus.failed); } - - action.complete(); } } diff --git a/uni/lib/model/providers/lazy/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart index e3655483e..57f184cfa 100644 --- a/uni/lib/model/providers/lazy/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/restaurant_fetcher.dart'; import 'package:uni/controller/local_storage/app_restaurant_database.dart'; import 'package:uni/model/entities/profile.dart'; @@ -28,28 +27,22 @@ class RestaurantProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final Completer action = Completer(); - getRestaurantsFromFetcher(action, session); - await action.future; + await fetchRestaurants(session); } - void getRestaurantsFromFetcher( - Completer action, Session session) async { + Future fetchRestaurants(Session session) async { try { - updateStatus(RequestStatus.busy); - final List restaurants = - await RestaurantFetcher().getRestaurants(session); - // Updates local database according to information fetched -- Restaurants + await RestaurantFetcher().getRestaurants(session); + final RestaurantDatabase db = RestaurantDatabase(); db.saveRestaurants(restaurants); + _restaurants = filterPastMeals(restaurants); - notifyListeners(); + updateStatus(RequestStatus.successful); } catch (e) { - Logger().e('Failed to get Restaurants: ${e.toString()}'); updateStatus(RequestStatus.failed); } - action.complete(); } } diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index ca37ec3e0..d0674800a 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart'; @@ -46,23 +45,12 @@ class ProfileProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final userInfoAction = Completer(); - fetchUserInfo(userInfoAction, session); - await userInfoAction.future; - - final Completer userFeesAction = Completer(); - fetchUserFees(userFeesAction, session); - - final Completer printBalanceAction = Completer(); - fetchUserPrintBalance(printBalanceAction, session); - - final Completer courseUnitsAction = Completer(); - fetchCourseUnitsAndCourseAverages(session, courseUnitsAction); + await fetchUserInfo(session); await Future.wait([ - userFeesAction.future, - printBalanceAction.future, - courseUnitsAction.future + fetchUserFees(session), + fetchUserPrintBalance(session), + fetchCourseUnitsAndCourseAverages(session) ]); if (status != RequestStatus.failed) { @@ -101,7 +89,7 @@ class ProfileProvider extends StateProviderNotifier { profile.courseUnits = await db.courseUnits(); } - fetchUserFees(Completer action, Session session) async { + Future fetchUserFees(Session session) async { try { final response = await FeesFetcher().getUserFeesResponse(session); @@ -113,8 +101,6 @@ class ProfileProvider extends StateProviderNotifier { await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); - - // Store fees info final profileDb = AppUserDataDatabase(); profileDb.saveUserFees(feesBalance, feesLimit); } @@ -129,13 +115,9 @@ class ProfileProvider extends StateProviderNotifier { _profile = newProfile; _feesRefreshTime = currentTime; - notifyListeners(); } catch (e) { - Logger().e('Failed to get Fees info'); updateStatus(RequestStatus.failed); } - - action.complete(); } Future storeRefreshTime(String db, String currentTime) async { @@ -144,7 +126,7 @@ class ProfileProvider extends StateProviderNotifier { refreshTimesDatabase.saveRefreshTime(db, currentTime); } - fetchUserPrintBalance(Completer action, Session session) async { + Future fetchUserPrintBalance(Session session) async { try { final response = await PrintFetcher().getUserPrintsResponse(session); final String printBalance = await getPrintsBalance(response); @@ -154,8 +136,6 @@ class ProfileProvider extends StateProviderNotifier { await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); - - // Store fees info final profileDb = AppUserDataDatabase(); profileDb.saveUserPrintBalance(printBalance); } @@ -170,19 +150,13 @@ class ProfileProvider extends StateProviderNotifier { _profile = newProfile; _printRefreshTime = currentTime; - notifyListeners(); } catch (e) { - Logger().e('Failed to get Print Balance'); updateStatus(RequestStatus.failed); } - - action.complete(); } - fetchUserInfo(Completer action, Session session) async { + Future fetchUserInfo(Session session) async { try { - updateStatus(RequestStatus.busy); - final profile = await ProfileFetcher.getProfile(session); final currentCourseUnits = await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); @@ -199,16 +173,11 @@ class ProfileProvider extends StateProviderNotifier { profileDb.insertUserData(_profile); } } catch (e) { - Logger().e('Failed to get User Info'); updateStatus(RequestStatus.failed); } - - action.complete(); } - fetchCourseUnitsAndCourseAverages( - Session session, Completer action) async { - updateStatus(RequestStatus.busy); + Future fetchCourseUnitsAndCourseAverages(Session session) async { try { final List courses = profile.courses; final List allCourseUnits = await AllCourseUnitsFetcher() @@ -226,11 +195,8 @@ class ProfileProvider extends StateProviderNotifier { await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); } } catch (e) { - Logger().e('Failed to get all user ucs: $e'); updateStatus(RequestStatus.failed); } - - action.complete(); } static Future fetchOrGetCachedProfilePicture( From 81c2128189daa2c4fda5e5c4ea7dff111916c921 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 12:56:18 +0100 Subject: [PATCH 082/100] Remove completers in exam provider and exam tests --- .../model/providers/lazy/exam_provider.dart | 44 ++++-------- .../view/exams/widgets/exam_filter_form.dart | 5 +- uni/lib/view/exams/widgets/exam_row.dart | 5 +- uni/test/integration/src/exams_page_test.dart | 17 +---- .../unit/providers/exams_provider_test.dart | 68 ++++--------------- 5 files changed, 33 insertions(+), 106 deletions(-) diff --git a/uni/lib/model/providers/lazy/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart index 0a21b0d61..161573f08 100644 --- a/uni/lib/model/providers/lazy/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/exam_fetcher.dart'; import 'package:uni/controller/local_storage/app_exams_database.dart'; @@ -32,9 +31,8 @@ class ExamProvider extends StateProviderNotifier { @override Future loadFromStorage() async { - setFilteredExams( - await AppSharedPreferences.getFilteredExams(), Completer()); - setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); + await setFilteredExams(await AppSharedPreferences.getFilteredExams()); + await setHiddenExams(await AppSharedPreferences.getHiddenExams()); final AppExamsDatabase db = AppExamsDatabase(); final List exams = await db.exams(); @@ -43,18 +41,15 @@ class ExamProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final Completer action = Completer(); - final ParserExams parserExams = ParserExams(); - final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - - fetchUserExams(action, parserExams, userPersistentInfo, profile, session, + await fetchUserExams( + ParserExams(), + await AppSharedPreferences.getPersistentUserInfo(), + profile, + session, profile.courseUnits); - await action.future; } Future fetchUserExams( - Completer action, ParserExams parserExams, Tuple2 userPersistentInfo, Profile profile, @@ -62,15 +57,11 @@ class ExamProvider extends StateProviderNotifier { List userUcs, ) async { try { - //need to get student course here - updateStatus(RequestStatus.busy); - final List exams = await ExamFetcher(profile.courses, userUcs) .extractExams(session, parserExams); exams.sort((exam1, exam2) => exam1.begin.compareTo(exam2.begin)); - // Updates local database according to the information fetched -- Exams if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppExamsDatabase db = AppExamsDatabase(); db.saveNewExams(exams); @@ -78,13 +69,9 @@ class ExamProvider extends StateProviderNotifier { _exams = exams; updateStatus(RequestStatus.successful); - notifyListeners(); } catch (e) { - Logger().e('Failed to get Exams'); updateStatus(RequestStatus.failed); } - - action.complete(); } updateFilteredExams() async { @@ -93,11 +80,9 @@ class ExamProvider extends StateProviderNotifier { notifyListeners(); } - setFilteredExams( - Map newFilteredExams, Completer action) async { - _filteredExamsTypes = Map.from(newFilteredExams); + Future setFilteredExams(Map newFilteredExams) async { AppSharedPreferences.saveFilteredExams(filteredExamsTypes); - action.complete(); + _filteredExamsTypes = Map.from(newFilteredExams); notifyListeners(); } @@ -108,23 +93,22 @@ class ExamProvider extends StateProviderNotifier { .toList(); } - setHiddenExams(List newHiddenExams, Completer action) async { + setHiddenExams(List newHiddenExams) async { _hiddenExams = List.from(newHiddenExams); - AppSharedPreferences.saveHiddenExams(hiddenExams); - action.complete(); + await AppSharedPreferences.saveHiddenExams(hiddenExams); notifyListeners(); } - toggleHiddenExam(String newExamId, Completer action) async { + Future toggleHiddenExam(String newExamId) async { _hiddenExams.contains(newExamId) ? _hiddenExams.remove(newExamId) : _hiddenExams.add(newExamId); + await AppSharedPreferences.saveHiddenExams(hiddenExams); notifyListeners(); - AppSharedPreferences.saveHiddenExams(hiddenExams); - action.complete(); } setExams(List newExams) { _exams = newExams; + notifyListeners(); } } diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 613cadecf..15436c4f3 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; @@ -29,8 +27,7 @@ class ExamFilterFormState extends State { child: const Text('Confirmar'), onPressed: () { Provider.of(context, listen: false) - .setFilteredExams(widget.filteredExamsTypes, Completer()); - + .setFilteredExams(widget.filteredExamsTypes); Navigator.pop(context); }) ], diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index 2ccb16329..a2de2c14e 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:add_2_calendar/add_2_calendar.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -76,8 +74,7 @@ class _ExamRowState extends State { onPressed: () => setState(() { Provider.of(context, listen: false) - .toggleHiddenExam( - widget.exam.id, Completer()); + .toggleHiddenExam(widget.exam.id); })), IconButton( icon: Icon(MdiIcons.calendarPlus, size: 30), diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index 9dfaa0aa2..a7bd8815f 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -1,6 +1,5 @@ // @dart=2.10 -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -83,17 +82,13 @@ void main() { expect(find.byKey(Key('$sopeExam-exam')), findsNothing); expect(find.byKey(Key('$mdisExam-exam')), findsNothing); - final Completer completer = Completer(); - examProvider.fetchUserExams( - completer, + await examProvider.fetchUserExams( ParserExams(), const Tuple2('', ''), profile, Session(authenticated: true), [sopeCourseUnit, sdisCourseUnit]); - await completer.future; - await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); @@ -122,27 +117,21 @@ void main() { expect(find.byKey(Key('$sdisExam-exam')), findsNothing); expect(find.byKey(Key('$sopeExam-exam')), findsNothing); - final Completer completer = Completer(); - examProvider.fetchUserExams( - completer, + await examProvider.fetchUserExams( ParserExams(), const Tuple2('', ''), profile, Session(authenticated: true), [sopeCourseUnit, sdisCourseUnit]); - await completer.future; - await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); expect(find.byIcon(Icons.filter_alt), findsOneWidget); - final Completer settingFilteredExams = Completer(); filteredExams['ExamDoesNotExist'] = true; - examProvider.setFilteredExams(filteredExams, settingFilteredExams); - await settingFilteredExams.future; + await examProvider.setFilteredExams(filteredExams); await tester.pumpAndSettle(); diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 9be691f5b..284f2c2b3 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -1,7 +1,5 @@ // @dart=2.10 -import 'dart:async'; - import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:tuple/tuple.dart'; @@ -66,14 +64,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam}); - final action = Completer(); - - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.exams.isNotEmpty, true); expect(provider.exams, [sopeExam]); @@ -84,14 +76,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam}); - final Completer action = Completer(); - - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); @@ -110,33 +96,22 @@ void main() { 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', 'feup'); - final Completer action = Completer(); - when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam, specialExam}); - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); }); test('When an error occurs while trying to obtain the exams', () async { - final Completer action = Completer(); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => throw Exception('RIP')); - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.failed); }); @@ -150,13 +125,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - final Completer action = Completer(); - - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); @@ -171,13 +141,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - final Completer action = Completer(); - - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, []); @@ -192,13 +157,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - final Completer action = Completer(); - - provider.fetchUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); From 459cbf237b5fe3cff13ef54e05736d2a8df85805 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 14:15:22 +0100 Subject: [PATCH 083/100] Remove completers from lecture provider and tests --- .../providers/lazy/lecture_provider.dart | 27 +++++-------------- .../integration/src/schedule_page_test.dart | 13 ++++----- .../unit/providers/lecture_provider_test.dart | 16 ++--------- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/uni/lib/model/providers/lazy/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart index 80f2bfd07..64f8e052a 100644 --- a/uni/lib/model/providers/lazy/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart'; @@ -31,43 +30,31 @@ class LectureProvider extends StateProviderNotifier { @override Future loadFromRemote(Session session, Profile profile) async { - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - final Completer action = Completer(); - fetchUserLectures(action, userPersistentInfo, session, profile); - await action.future; + await fetchUserLectures( + await AppSharedPreferences.getPersistentUserInfo(), session, profile); } - void fetchUserLectures( - Completer action, - Tuple2 userPersistentInfo, - Session session, - Profile profile, + Future fetchUserLectures(Tuple2 userPersistentInfo, + Session session, Profile profile, {ScheduleFetcher? fetcher}) async { try { - updateStatus(RequestStatus.busy); - final List lectures = - await getLecturesFromFetcherOrElse(fetcher, session, profile); + await getLecturesFromFetcherOrElse(fetcher, session, profile); - // Updates local database according to the information fetched -- Lectures if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { final AppLecturesDatabase db = AppLecturesDatabase(); db.saveNewLectures(lectures); } _lectures = lectures; - notifyListeners(); updateStatus(RequestStatus.successful); } catch (e) { - Logger().e('Failed to get Schedule: ${e.toString()}'); updateStatus(RequestStatus.failed); } - action.complete(); } - Future> getLecturesFromFetcherOrElse( - ScheduleFetcher? fetcher, Session session, Profile profile) => + Future> getLecturesFromFetcherOrElse(ScheduleFetcher? fetcher, + Session session, Profile profile) => (fetcher?.getLectures(session, profile)) ?? getLectures(session, profile); Future> getLectures(Session session, Profile profile) { diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index 7451d4b49..e257e41a9 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -1,6 +1,5 @@ // @dart=2.10 -import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -63,10 +62,8 @@ void main() { expect(find.byKey(const Key(scheduleSlotTimeKey1)), findsNothing); expect(find.byKey(const Key(scheduleSlotTimeKey2)), findsNothing); - final Completer completer = Completer(); - scheduleProvider.fetchUserLectures(completer, const Tuple2('', ''), + await scheduleProvider.fetchUserLectures(const Tuple2('', ''), Session(authenticated: true), profile); - await completer.future; await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); @@ -92,11 +89,11 @@ void main() { when(mockResponse.body).thenReturn(mockJson); when(mockResponse.statusCode).thenReturn(200); when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) + headers: anyNamed('headers'))) .thenAnswer((_) async => badMockResponse); when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) + headers: anyNamed('headers'))) .thenAnswer((_) async => mockResponse); await testSchedule(tester); @@ -108,11 +105,11 @@ void main() { when(mockResponse.body).thenReturn(mockHtml); when(mockResponse.statusCode).thenReturn(200); when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) + headers: anyNamed('headers'))) .thenAnswer((_) async => mockResponse); when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) + headers: anyNamed('headers'))) .thenAnswer((_) async => badMockResponse); await testSchedule(tester); diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index d05496119..400c51281 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -1,7 +1,5 @@ // @dart=2.10 -import 'dart:async'; - import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:tuple/tuple.dart'; @@ -43,31 +41,21 @@ void main() { }); test('When given a single schedule', () async { - final Completer action = Completer(); - when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => [lecture1, lecture2]); - provider.fetchUserLectures(action, userPersistentInfo, session, profile, + await provider.fetchUserLectures(userPersistentInfo, session, profile, fetcher: fetcherMock); - expect(provider.status, RequestStatus.busy); - - await action.future; expect(provider.lectures, [lecture1, lecture2]); expect(provider.status, RequestStatus.successful); }); test('When an error occurs while trying to obtain the schedule', () async { - final Completer action = Completer(); - when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => throw Exception('💥')); - provider.fetchUserLectures(action, userPersistentInfo, session, profile); - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserLectures(userPersistentInfo, session, profile); expect(provider.status, RequestStatus.failed); }); From 26e5207dea225d5c30f87057b8f529fdc4c65831 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 14:34:10 +0100 Subject: [PATCH 084/100] Remove completers from the login --- .../controller/parsers/parser_session.dart | 6 +- .../providers/startup/session_provider.dart | 69 ++++++++++--------- uni/lib/view/login/login.dart | 20 +++--- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/uni/lib/controller/parsers/parser_session.dart b/uni/lib/controller/parsers/parser_session.dart index fc132d344..8a32b4f12 100644 --- a/uni/lib/controller/parsers/parser_session.dart +++ b/uni/lib/controller/parsers/parser_session.dart @@ -1,8 +1,8 @@ import 'package:html/parser.dart'; -bool isPasswordExpired(String htmlBody){ +bool isPasswordExpired(String htmlBody) { final document = parse(htmlBody); final alerts = document.querySelectorAll('.aviso-invalidado'); - if(alerts.length < 2) return false; - return alerts[1].text.contains('A sua senha de acesso encontra-se expirada'); + return alerts.length >= 2 && + alerts[1].text.contains('A sua senha de acesso encontra-se expirada'); } diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 0866cb720..b927cee46 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -19,9 +19,9 @@ class SessionProvider extends StateProviderNotifier { SessionProvider() : super( - dependsOnSession: false, - cacheDuration: null, - initialStatus: RequestStatus.none); + dependsOnSession: false, + cacheDuration: null, + initialStatus: RequestStatus.none); Session get session => _session; @@ -36,43 +36,44 @@ class SessionProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } - login(Completer action, String username, String password, - List faculties, persistentSession) async { - try { - updateStatus(RequestStatus.busy); + Future login(String username, String password, List faculties, + persistentSession) async { + _faculties = faculties; + + updateStatus(RequestStatus.busy); - _faculties = faculties; + try { _session = await NetworkRouter.login( username, password, faculties, persistentSession); - - if (_session.authenticated) { - if (persistentSession) { - await AppSharedPreferences.savePersistentUserInfo( - username, password, faculties); - } - Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); - - await acceptTermsAndConditions(); - updateStatus(RequestStatus.successful); - } else { - final String responseHtml = - await NetworkRouter.loginInSigarra(username, password, faculties); - if (isPasswordExpired(responseHtml)) { - action.completeError(ExpiredCredentialsException()); - } else { - action.completeError(WrongCredentialsException()); - } - updateStatus(RequestStatus.failed); - } } catch (e) { - // No internet connection or server down - action.completeError(InternetStatusException()); updateStatus(RequestStatus.failed); + throw InternetStatusException(); } - notifyListeners(); - action.complete(); + if (_session.authenticated) { + if (persistentSession) { + await AppSharedPreferences.savePersistentUserInfo( + username, password, faculties); + } + + Future.delayed(const Duration(seconds: 20), + () => {NotificationManager().initializeNotifications()}); + + await acceptTermsAndConditions(); + updateStatus(RequestStatus.successful); + return; + } + + final String responseHtml = + await NetworkRouter.loginInSigarra(username, password, faculties); + + updateStatus(RequestStatus.failed); + + if (isPasswordExpired(responseHtml)) { + throw ExpiredCredentialsException(); + } else { + throw WrongCredentialsException(); + } } reLogin(String username, String password, List faculties, @@ -83,7 +84,7 @@ class SessionProvider extends StateProviderNotifier { if (session.authenticated) { Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); updateStatus(RequestStatus.successful); action?.complete(); } else { diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 848247506..fad9037ab 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -45,28 +45,30 @@ class LoginPageViewState extends State { bool _keepSignedIn = true; bool _obscurePasswordInput = true; - void _login(BuildContext context) { + void _login(BuildContext context) async { final stateProviders = StateProviders.fromContext(context); final sessionProvider = stateProviders.sessionProvider; if (sessionProvider.status != RequestStatus.busy && _formKey.currentState!.validate()) { final user = usernameController.text.trim(); final pass = passwordController.text.trim(); - final completer = Completer(); - sessionProvider.login(completer, user, pass, faculties, _keepSignedIn); - - completer.future.then((_) { - handleLogin(sessionProvider.status, context); - }).catchError((error) { + try { + await sessionProvider.login(user, pass, faculties, _keepSignedIn); + if (context.mounted) { + handleLogin(sessionProvider.status, context); + } + } catch (error) { if (error is ExpiredCredentialsException) { updatePasswordDialog(); } else if (error is InternetStatusException) { ToastMessage.warning(context, error.message); + } else if (error is WrongCredentialsException) { + ToastMessage.error(context, error.message); } else { - ToastMessage.error(context, error.message ?? 'Erro no login'); + ToastMessage.error(context, 'Erro no login'); } - }); + } } } From 9e6b929c50c3138b74208d6be23eabe7624d80e9 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 14:36:15 +0100 Subject: [PATCH 085/100] Remove completers in session --- .../providers/startup/session_provider.dart | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index b927cee46..7d38da794 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -19,9 +19,9 @@ class SessionProvider extends StateProviderNotifier { SessionProvider() : super( - dependsOnSession: false, - cacheDuration: null, - initialStatus: RequestStatus.none); + dependsOnSession: false, + cacheDuration: null, + initialStatus: RequestStatus.none); Session get session => _session; @@ -57,7 +57,7 @@ class SessionProvider extends StateProviderNotifier { } Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); await acceptTermsAndConditions(); updateStatus(RequestStatus.successful); @@ -65,7 +65,7 @@ class SessionProvider extends StateProviderNotifier { } final String responseHtml = - await NetworkRouter.loginInSigarra(username, password, faculties); + await NetworkRouter.loginInSigarra(username, password, faculties); updateStatus(RequestStatus.failed); @@ -76,19 +76,16 @@ class SessionProvider extends StateProviderNotifier { } } - reLogin(String username, String password, List faculties, - {Completer? action}) async { + reLogin(String username, String password, List faculties) async { try { - updateStatus(RequestStatus.busy); _session = await NetworkRouter.login(username, password, faculties, true); if (session.authenticated) { Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + () => {NotificationManager().initializeNotifications()}); updateStatus(RequestStatus.successful); - action?.complete(); } else { - handleFailedReLogin(action); + handleFailedReLogin(); } } catch (e) { _session = Session( @@ -99,12 +96,11 @@ class SessionProvider extends StateProviderNotifier { cookies: '', persistentSession: true); - handleFailedReLogin(action); + handleFailedReLogin(); } } - handleFailedReLogin(Completer? action) { - action?.completeError(RequestStatus.failed); + handleFailedReLogin() { if (!session.persistentSession) { return NavigationService.logout(); } From c36132df08e579b7ef33d15ddd7056f5b827a1f7 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Wed, 19 Jul 2023 15:33:17 +0100 Subject: [PATCH 086/100] Fix terms acceptance logic --- .../load_static/terms_and_conditions.dart | 29 +++++---- .../about/widgets/terms_and_conditions.dart | 2 +- uni/lib/view/splash/splash.dart | 25 ++++---- .../widgets/terms_and_condition_dialog.dart | 62 ++++++++----------- 4 files changed, 56 insertions(+), 62 deletions(-) diff --git a/uni/lib/controller/load_static/terms_and_conditions.dart b/uni/lib/controller/load_static/terms_and_conditions.dart index 6aa23ae88..dd1cecb17 100644 --- a/uni/lib/controller/load_static/terms_and_conditions.dart +++ b/uni/lib/controller/load_static/terms_and_conditions.dart @@ -7,14 +7,15 @@ import 'package:http/http.dart' as http; import 'package:logger/logger.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -/// Returns the content of the Terms and Conditions file. +/// Returns the content of the Terms and Conditions remote file, +/// or the local one if the remote file is not available. /// /// If this operation is unsuccessful, an error message is returned. -Future readTermsAndConditions() async { +Future fetchTermsAndConditions() async { if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { try { - const String url = - 'https://raw.githubusercontent.com/NIAEFEUP/project-schrodinger/develop/uni/assets/text/TermsAndConditions.md'; + const String url = 'https://raw.githubusercontent.com/NIAEFEUP/' + 'uni/develop/uni/assets/text/TermsAndConditions.md'; final http.Response response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { return response.body; @@ -23,6 +24,7 @@ Future readTermsAndConditions() async { Logger().e('Failed to fetch Terms and Conditions: ${e.toString()}'); } } + try { return await rootBundle.loadString('assets/text/TermsAndConditions.md'); } catch (e) { @@ -32,16 +34,17 @@ Future readTermsAndConditions() async { } } -/// Checks if the current Terms and Conditions have been accepted by the user. -/// -/// Returns true if the current Terms and Conditions have been accepted, -/// false otherwise. +/// Checks if the current Terms and Conditions have been accepted by the user, +/// by fetching the current terms, hashing them and comparing with the stored hash. +/// Sets the acceptance to false if the terms have changed, or true if they haven't. +/// Returns the updated value. Future updateTermsAndConditionsAcceptancePreference() async { final hash = await AppSharedPreferences.getTermsAndConditionHash(); - final acceptance = await AppSharedPreferences.areTermsAndConditionsAccepted(); - final termsAndConditions = await readTermsAndConditions(); + final termsAndConditions = await fetchTermsAndConditions(); final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); + if (hash == null) { + await AppSharedPreferences.setTermsAndConditionsAcceptance(true); await AppSharedPreferences.setTermsAndConditionHash(currentHash); return true; } @@ -49,14 +52,16 @@ Future updateTermsAndConditionsAcceptancePreference() async { if (currentHash != hash) { await AppSharedPreferences.setTermsAndConditionsAcceptance(false); await AppSharedPreferences.setTermsAndConditionHash(currentHash); + return false; } - return currentHash != hash || !acceptance; + await AppSharedPreferences.setTermsAndConditionsAcceptance(true); + return true; } /// Accepts the current Terms and Conditions. Future acceptTermsAndConditions() async { - final termsAndConditions = await readTermsAndConditions(); + final termsAndConditions = await fetchTermsAndConditions(); final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); await AppSharedPreferences.setTermsAndConditionHash(currentHash); await AppSharedPreferences.setTermsAndConditionsAcceptance(true); diff --git a/uni/lib/view/about/widgets/terms_and_conditions.dart b/uni/lib/view/about/widgets/terms_and_conditions.dart index 6f1407a36..4be3ff71f 100644 --- a/uni/lib/view/about/widgets/terms_and_conditions.dart +++ b/uni/lib/view/about/widgets/terms_and_conditions.dart @@ -11,7 +11,7 @@ class TermsAndConditions extends StatelessWidget { @override Widget build(BuildContext context) { - final Future termsAndConditionsFuture = readTermsAndConditions(); + final Future termsAndConditionsFuture = fetchTermsAndConditions(); return FutureBuilder( future: termsAndConditionsFuture, builder: diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index 5b0ad0d22..975f1d3ba 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -28,7 +28,7 @@ class SplashScreenState extends State { void didChangeDependencies() { super.didChangeDependencies(); stateProviders = StateProviders.fromContext(context); - startTimeAndChangeRoute(); + changeRouteAccordingToLoginAndTerms(); } @override @@ -38,6 +38,7 @@ class SplashScreenState extends State { MediaQuery.platformBrightnessOf(context) == Brightness.dark ? applicationDarkTheme : applicationLightTheme; + return Theme( data: systemTheme, child: Builder( @@ -100,33 +101,33 @@ class SplashScreenState extends State { } // Redirects the user to the proper page depending on his login input. - void startTimeAndChangeRoute() async { - MaterialPageRoute nextRoute; + void changeRouteAccordingToLoginAndTerms() async { final Tuple2 userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); final String userName = userPersistentInfo.item1; final String password = userPersistentInfo.item2; + + MaterialPageRoute nextRoute; if (userName != '' && password != '') { nextRoute = - await getTermsAndConditions(userName, password, stateProviders); + await termsAndConditionsRoute(userName, password, stateProviders); } else { await acceptTermsAndConditions(); nextRoute = MaterialPageRoute(builder: (context) => const LoginPageView()); } - if (!mounted) { - return; + + if (mounted) { + Navigator.pushReplacement(context, nextRoute); } - Navigator.pushReplacement(context, nextRoute); } - Future getTermsAndConditions( + Future termsAndConditionsRoute( String userName, String password, StateProviders stateProviders) async { - final completer = Completer(); - await TermsAndConditionDialog.build(context, completer, userName, password); - final state = await completer.future; + final termsAcceptance = await TermsAndConditionDialog.buildIfTermsChanged( + context, userName, password); - switch (state) { + switch (termsAcceptance) { case TermsAndConditionsState.accepted: if (mounted) { final List faculties = diff --git a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart index 81abb0377..719b002ab 100644 --- a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart +++ b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart @@ -11,20 +11,19 @@ enum TermsAndConditionsState { accepted, rejected } class TermsAndConditionDialog { TermsAndConditionDialog._(); - static Future build( - BuildContext context, - Completer routeCompleter, - String userName, - String password) async { - final acceptance = await updateTermsAndConditionsAcceptancePreference(); - if (acceptance) { + static Future buildIfTermsChanged( + BuildContext context, String userName, String password) async { + final termsAreAccepted = + await updateTermsAndConditionsAcceptancePreference(); + + if (!termsAreAccepted) { + final routeCompleter = Completer(); SchedulerBinding.instance.addPostFrameCallback((timestamp) => _buildShowDialog(context, routeCompleter, userName, password)); - } else { - routeCompleter.complete(TermsAndConditionsState.accepted); + return routeCompleter.future; } - return acceptance; + return TermsAndConditionsState.accepted; } static Future _buildShowDialog( @@ -41,24 +40,16 @@ class TermsAndConditionDialog { style: Theme.of(context).textTheme.headlineSmall), content: Column( children: [ - Expanded( - child: SingleChildScrollView( - child: ListBody( - children: [ - Container( - margin: const EdgeInsets.only(bottom: 10), - child: const Text( - '''Os Termos e Condições da aplicação mudaram desde a última vez que a abriste:'''), - ), - const TermsAndConditions() - ], - ), - ), + const Expanded( + child: SingleChildScrollView(child: TermsAndConditions()), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - TextButton( + ElevatedButton( onPressed: () async { Navigator.of(context).pop(); routeCompleter @@ -66,11 +57,13 @@ class TermsAndConditionDialog { await AppSharedPreferences .setTermsAndConditionsAcceptance(true); }, - child: Text( - 'Aceito os novos Termos e Condições', - style: getTextMethod(context), + child: const Text( + 'Aceito', )), - TextButton( + const SizedBox( + width: 10, + ), + ElevatedButton( onPressed: () async { Navigator.of(context).pop(); routeCompleter @@ -78,9 +71,8 @@ class TermsAndConditionDialog { await AppSharedPreferences .setTermsAndConditionsAcceptance(false); }, - child: Text( - 'Rejeito os novos Termos e Condições', - style: getTextMethod(context), + child: const Text( + 'Rejeito', )), ], ) @@ -89,8 +81,4 @@ class TermsAndConditionDialog { ); }); } - - static TextStyle getTextMethod(BuildContext context) { - return Theme.of(context).textTheme.titleLarge!; - } } From a93b41778555b98322d90eb92da15bda8525d094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Costa?= Date: Fri, 21 Jul 2023 02:13:41 +0100 Subject: [PATCH 087/100] Add timeout to requests made by getWithCookies --- .../controller/networking/network_router.dart | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index f92b85e7b..2219400e0 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -17,8 +17,8 @@ extension UriString on String { /// Manages the networking of the app. class NetworkRouter { static http.Client? httpClient; - static const int loginRequestTimeout = 20; - static Lock loginLock = Lock(); + static const int _requestTimeout = 5; + static final Lock _loginLock = Lock(); /// Creates an authenticated [Session] on the given [faculty] with the /// given username [user] and password [pass]. @@ -29,7 +29,7 @@ class NetworkRouter { final http.Response response = await http.post(url.toUri(), body: { 'pv_login': user, 'pv_password': pass - }).timeout(const Duration(seconds: loginRequestTimeout)); + }).timeout(const Duration(seconds: _requestTimeout)); if (response.statusCode == 200) { final Session session = Session.fromLogin(response, faculties); session.persistentSession = persistentSession; @@ -50,7 +50,7 @@ class NetworkRouter { /// Determines if a re-login with the [session] is possible. static Future reLogin(Session session) { - return loginLock.synchronized(() async { + return _loginLock.synchronized(() async { if (!session.persistentSession) { return false; } @@ -74,7 +74,7 @@ class NetworkRouter { final http.Response response = await http.post(url.toUri(), body: { 'pv_login': session.studentNumber, 'pv_password': await AppSharedPreferences.getUserPassword(), - }).timeout(const Duration(seconds: loginRequestTimeout)); + }).timeout(const Duration(seconds: _requestTimeout)); final responseBody = json.decode(response.body); if (response.statusCode == 200 && responseBody['authenticated']) { session.authenticated = true; @@ -99,7 +99,7 @@ class NetworkRouter { final response = await http.post(url.toUri(), body: { 'p_user': user, 'p_pass': pass - }).timeout(const Duration(seconds: loginRequestTimeout)); + }).timeout(const Duration(seconds: _requestTimeout)); return response.body; } @@ -141,8 +141,12 @@ class NetworkRouter { headers['cookie'] = session.cookies; final http.Response response = await (httpClient != null - ? httpClient!.get(url.toUri(), headers: headers) - : http.get(url.toUri(), headers: headers)); + ? httpClient! + .get(url.toUri(), headers: headers) + .timeout(const Duration(seconds: _requestTimeout)) + : http + .get(url.toUri(), headers: headers) + .timeout(const Duration(seconds: _requestTimeout))); if (response.statusCode == 200) { return response; } else if (response.statusCode == 403 && !(await userLoggedIn(session))) { @@ -150,14 +154,15 @@ class NetworkRouter { final bool reLoginSuccessful = await reLogin(session); if (reLoginSuccessful) { headers['cookie'] = session.cookies; - return http.get(url.toUri(), headers: headers); + return http.get(url.toUri(), headers: headers).timeout(const Duration(seconds: _requestTimeout)); } else { NavigationService.logout(); Logger().e('Login failed'); return Future.error('Login failed'); } } else { - return Future.error('HTTP Error ${response.statusCode}'); + Logger().e('Connection error ${response.statusCode}'); + return Future.error('Connection error ${response.statusCode}'); } } @@ -194,7 +199,7 @@ class NetworkRouter { final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; final response = await http .get(url.toUri()) - .timeout(const Duration(seconds: loginRequestTimeout)); + .timeout(const Duration(seconds: _requestTimeout)); if (response.statusCode == 200) { Logger().i("Logout Successful"); } else { From c5871bb48516ad341c145190b8d7335c22263db9 Mon Sep 17 00:00:00 2001 From: thePeras Date: Sat, 22 Jul 2023 10:45:53 +0100 Subject: [PATCH 088/100] Update GitHub actions versions --- .github/workflows/deploy.yaml | 9 +++++---- .github/workflows/format_lint_test.yaml | 14 ++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6289971b7..94a4a6309 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -46,10 +46,11 @@ jobs: working-directory: ./uni steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v3 with: java-version: ${{env.JAVA_VERSION}} - - uses: subosito/flutter-action@v1 + distribution: 'zulu' + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} @@ -77,7 +78,7 @@ jobs: flutter build appbundle - name: Upload App Bundle - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: appbundle if-no-files-found: error @@ -90,7 +91,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Get App Bundle - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: appbundle diff --git a/.github/workflows/format_lint_test.yaml b/.github/workflows/format_lint_test.yaml index fb24e2ebc..636ccc151 100644 --- a/.github/workflows/format_lint_test.yaml +++ b/.github/workflows/format_lint_test.yaml @@ -13,7 +13,7 @@ jobs: working-directory: ./uni steps: - uses: actions/checkout@v3 - - uses: subosito/flutter-action@v1 + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} @@ -28,15 +28,16 @@ jobs: working-directory: ./uni steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v3 with: java-version: ${{ env.JAVA_VERSION }} - - uses: subosito/flutter-action@v1 + distribution: 'zulu' + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} - name: Cache pub dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.FLUTTER_HOME }}/.pub-cache key: ${{ runner.os }}-pub-${{ github.ref }}-${{ hashFiles('**/pubspec.lock') }} @@ -53,10 +54,11 @@ jobs: working-directory: ./uni steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v3 with: java-version: ${{ env.JAVA_VERSION }} - - uses: subosito/flutter-action@v1 + distribution: 'zulu' + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} From 0ff1f0d02051252b165a41050a0dbe8b4cbb53e1 Mon Sep 17 00:00:00 2001 From: Bruno Mendes <61701401+bdmendes@users.noreply.github.com> Date: Sat, 22 Jul 2023 11:37:30 +0100 Subject: [PATCH 089/100] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 88107466a..13787ab54 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@

-[![Build badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/project-schrodinger/test_lint.yaml?style=for-the-badge)](https://github.com/NIAEFEUP/project-schrodinger/actions) -[![Deploy badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/project-schrodinger/deploy.yaml?label=Deploy&style=for-the-badge)](https://github.com/NIAEFEUP/project-schrodinger/actions) -[![License badge](https://img.shields.io/github/license/NIAEFEUP/project-schrodinger?style=for-the-badge)](https://github.com/NIAEFEUP/project-schrodinger/blob/master/LICENSE) +[![Build badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/uni/format_lint_test.yaml?style=for-the-badge)](https://github.com/NIAEFEUP/uni/actions) +[![Deploy badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/uni/deploy.yaml?label=Deploy&style=for-the-badge)](https://github.com/NIAEFEUP/uni/actions) +[![License badge](https://img.shields.io/github/license/NIAEFEUP/uni?style=for-the-badge)](https://github.com/NIAEFEUP/uni/blob/develop/LICENSE) Get it on Google Play @@ -31,4 +31,4 @@ This application is licensed under the [GNU General Public License v3.0](./LICEN Contributions are welcome, and can be made by opening a pull request. Please note, however, that a university's account is required to access most of the app's features. -For further information about the project structure, please refer to [the app's README file](./uni/README.md). \ No newline at end of file +For further information about the project structure, please refer to [the app's README file](./uni/README.md). From d130c6b63cbbcd005bdc39e2ebb8d087835d97f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Costa?= Date: Sat, 22 Jul 2023 20:18:18 +0100 Subject: [PATCH 090/100] Increase default timeout to 10s and add parameterized timeout --- uni/lib/controller/networking/network_router.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index 2219400e0..2f1190531 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -17,7 +17,7 @@ extension UriString on String { /// Manages the networking of the app. class NetworkRouter { static http.Client? httpClient; - static const int _requestTimeout = 5; + static const int _requestTimeout = 10; static final Lock _loginLock = Lock(); /// Creates an authenticated [Session] on the given [faculty] with the @@ -120,7 +120,8 @@ class NetworkRouter { /// Makes an authenticated GET request with the given [session] to the /// resource located at [url] with the given [query] parameters. static Future getWithCookies( - String baseUrl, Map query, Session session) async { + String baseUrl, Map query, Session session, + {int timeout = _requestTimeout}) async { final loginSuccessful = await session.loginRequest; if (loginSuccessful != null && !loginSuccessful) { return Future.error('Login failed'); @@ -140,13 +141,14 @@ class NetworkRouter { final Map headers = {}; headers['cookie'] = session.cookies; + final timeoutDuration = Duration(seconds: timeout); final http.Response response = await (httpClient != null ? httpClient! .get(url.toUri(), headers: headers) - .timeout(const Duration(seconds: _requestTimeout)) + .timeout(timeoutDuration) : http .get(url.toUri(), headers: headers) - .timeout(const Duration(seconds: _requestTimeout))); + .timeout(timeoutDuration)); if (response.statusCode == 200) { return response; } else if (response.statusCode == 403 && !(await userLoggedIn(session))) { @@ -154,7 +156,9 @@ class NetworkRouter { final bool reLoginSuccessful = await reLogin(session); if (reLoginSuccessful) { headers['cookie'] = session.cookies; - return http.get(url.toUri(), headers: headers).timeout(const Duration(seconds: _requestTimeout)); + return http + .get(url.toUri(), headers: headers) + .timeout(timeoutDuration); } else { NavigationService.logout(); Logger().e('Login failed'); From e41da1d63a285f891b38b2bdf60c73a4fae58b8c Mon Sep 17 00:00:00 2001 From: Sirze01 Date: Sat, 22 Jul 2023 20:17:23 +0000 Subject: [PATCH 091/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 180d31cef..70cbdace6 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.38+156 \ No newline at end of file +1.5.39+157 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 391ae70b1..f9a1106f7 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.38+156 +version: 1.5.39+157 environment: sdk: ">=2.17.1 <3.0.0" From 9e4382e8a0a4cd4e556961dd53e1f374dea662e2 Mon Sep 17 00:00:00 2001 From: LuisDuarte1 Date: Sun, 23 Jul 2023 10:33:33 +0000 Subject: [PATCH 092/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 70cbdace6..7f39376f5 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.39+157 \ No newline at end of file +1.5.40+158 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index f9a1106f7..a6e078081 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.39+157 +version: 1.5.40+158 environment: sdk: ">=2.17.1 <3.0.0" From 0858b6a216deed162c392b9ec9e283257270097c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Costa?= Date: Sun, 23 Jul 2023 12:00:31 +0100 Subject: [PATCH 093/100] Make use Duration(s) instead of creating them based on arguments --- .../controller/networking/network_router.dart | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index f5eb4d18c..3ad482bd4 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -21,7 +21,7 @@ class NetworkRouter { static http.Client? httpClient; /// The timeout for Sigarra login requests. - static const int _requestTimeout = 10; + static const Duration _requestTimeout = Duration(seconds: 10); /// The mutual exclusion primitive for login requests. static final Lock _loginLock = Lock(); @@ -38,7 +38,7 @@ class NetworkRouter { final http.Response response = await http.post(url.toUri(), body: { 'pv_login': username, 'pv_password': password - }).timeout(const Duration(seconds: _requestTimeout)); + }).timeout(_requestTimeout); if (response.statusCode != 200) { Logger().e("Login failed with status code ${response.statusCode}"); @@ -79,10 +79,8 @@ class NetworkRouter { final String url = '${NetworkRouter.getBaseUrls(faculties)[0]}vld_validacao.validacao'; - final response = await http.post(url.toUri(), body: { - 'p_user': user, - 'p_pass': pass - }).timeout(const Duration(seconds: _requestTimeout)); + final response = await http.post(url.toUri(), + body: {'p_user': user, 'p_pass': pass}).timeout(_requestTimeout); return response.body; }); @@ -109,7 +107,7 @@ class NetworkRouter { /// and the session is updated. static Future getWithCookies( String baseUrl, Map query, Session session, - {int timeout = _requestTimeout}) async { + {Duration timeout = _requestTimeout}) async { if (!baseUrl.contains('?')) { baseUrl += '?'; } @@ -124,13 +122,10 @@ class NetworkRouter { final Map headers = {}; headers['cookie'] = session.cookies; - final Duration timeoutDuration = Duration(seconds: timeout); final http.Response response = await (httpClient != null - ? httpClient! - .get(url.toUri(), headers: headers) - .timeout(timeoutDuration) + ? httpClient!.get(url.toUri(), headers: headers).timeout(timeout) : http.get(url.toUri(), headers: headers)) - .timeout(timeoutDuration); + .timeout(timeout); if (response.statusCode == 200) { return response; @@ -149,15 +144,14 @@ class NetworkRouter { session.cookies = newSession.cookies; headers['cookie'] = session.cookies; - return http.get(url.toUri(), headers: headers).timeout(timeoutDuration); + return http.get(url.toUri(), headers: headers).timeout(timeout); } else { // If the user is logged in but still got a 403, they are forbidden to access the resource // or the login was invalid at the time of the request, but other thread re-authenticated. // Since we do not know which one is the case, we try again. headers['cookie'] = session.cookies; - final response = await http - .get(url.toUri(), headers: headers) - .timeout(timeoutDuration); + final response = + await http.get(url.toUri(), headers: headers).timeout(timeout); return response.statusCode == 200 ? Future.value(response) : Future.error('HTTP Error: ${response.statusCode}'); @@ -201,9 +195,7 @@ class NetworkRouter { static Future killSigarraAuthentication(List faculties) async { return _loginLock.synchronized(() async { final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; - final response = await http - .get(url.toUri()) - .timeout(const Duration(seconds: _requestTimeout)); + final response = await http.get(url.toUri()).timeout(_requestTimeout); if (response.statusCode == 200) { Logger().i("Logout Successful"); From 91976600db72b02b1619b93496c1abcb1d9ac3f8 Mon Sep 17 00:00:00 2001 From: thePeras Date: Sun, 23 Jul 2023 11:41:57 +0000 Subject: [PATCH 094/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 7f39376f5..0d0d3b73f 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.40+158 \ No newline at end of file +1.5.41+159 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index a6e078081..06ba3e550 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.40+158 +version: 1.5.41+159 environment: sdk: ">=2.17.1 <3.0.0" From 9870485c27569e71740c9ec23b13d247862dd9ea Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Fri, 21 Jul 2023 12:32:38 +0100 Subject: [PATCH 095/100] Update update time only if successfull --- uni/lib/model/providers/state_provider_notifier.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index b854d8fd6..320b37d99 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -75,9 +75,11 @@ abstract class StateProviderNotifier extends ChangeNotifier { // No online activity from provider updateStatus(RequestStatus.successful); } else { - _lastUpdateTime = DateTime.now(); - await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); + if (_status == RequestStatus.successful) { + _lastUpdateTime = DateTime.now(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); + } notifyListeners(); } } From ee7374c17ec9d62c74895300051c10fe3deef301 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 22 Jul 2023 19:02:14 +0100 Subject: [PATCH 096/100] Rewrite method to make it more clear --- .../providers/state_provider_notifier.dart | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 320b37d99..bddbde907 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -43,44 +43,43 @@ abstract class StateProviderNotifier extends ChangeNotifier { Future _loadFromRemote(Session session, Profile profile, {bool force = false}) async { - final bool hasConnectivity = - await Connectivity().checkConnectivity() != ConnectivityResult.none; final shouldReload = force || _lastUpdateTime == null || cacheDuration == null || DateTime.now().difference(_lastUpdateTime!) > cacheDuration!; - if (shouldReload) { - if (hasConnectivity) { - updateStatus(RequestStatus.busy); - await loadFromRemote(session, profile); - if (_status == RequestStatus.successful) { - Logger().i("Loaded $runtimeType info from remote"); - } else if (_status == RequestStatus.failed) { - Logger().e("Failed to load $runtimeType info from remote"); - } else { - Logger().w( - "$runtimeType remote load method did not update request status"); - } - } else { - Logger().w("No internet connection; skipping $runtimeType remote load"); - } - } else { + if (!shouldReload) { Logger().i( "Last info for $runtimeType is within cache period (last updated on $_lastUpdateTime); " "skipping remote load"); + _status = RequestStatus.successful; + return; } - if (!shouldReload || !hasConnectivity || _status == RequestStatus.busy) { - // No online activity from provider - updateStatus(RequestStatus.successful); - } else { - if (_status == RequestStatus.successful) { - _lastUpdateTime = DateTime.now(); - await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); - } + final bool hasConnectivity = + await Connectivity().checkConnectivity() != ConnectivityResult.none; + + if (!hasConnectivity) { + Logger().w("No internet connection; skipping $runtimeType remote load"); + _status = RequestStatus.successful; + return; + } + + updateStatus(RequestStatus.busy); + + await loadFromRemote(session, profile); + + if (_status == RequestStatus.successful) { + Logger().i("Loaded $runtimeType info from remote"); + _lastUpdateTime = DateTime.now(); notifyListeners(); + await AppSharedPreferences.setLastDataClassUpdateTime( + runtimeType.toString(), _lastUpdateTime!); + } else if (_status == RequestStatus.failed) { + Logger().e("Failed to load $runtimeType info from remote"); + } else { + Logger() + .w("$runtimeType remote load method did not update request status"); } } From 19ad5f9f12d03d8824e599702d01c5635161ec1a Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 22 Jul 2023 20:21:16 +0100 Subject: [PATCH 097/100] Update status via wrapper to ensure notification --- uni/lib/model/providers/state_provider_notifier.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index bddbde907..5d8acc1b7 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -52,7 +52,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { Logger().i( "Last info for $runtimeType is within cache period (last updated on $_lastUpdateTime); " "skipping remote load"); - _status = RequestStatus.successful; + updateStatus(RequestStatus.successful); return; } @@ -61,7 +61,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { if (!hasConnectivity) { Logger().w("No internet connection; skipping $runtimeType remote load"); - _status = RequestStatus.successful; + updateStatus(RequestStatus.successful); return; } From 9804c0d5d9c77d019a10d97a3b26dec8e13456f6 Mon Sep 17 00:00:00 2001 From: Sirze01 Date: Mon, 24 Jul 2023 23:30:41 +0000 Subject: [PATCH 098/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index 0d0d3b73f..de9a1fc68 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.41+159 \ No newline at end of file +1.5.42+160 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 06ba3e550..ebf1069a8 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.41+159 +version: 1.5.42+160 environment: sdk: ">=2.17.1 <3.0.0" From 7ee23b31637a3696481dd89f64c4acffe7cfad7c Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sun, 23 Jul 2023 13:09:27 +0100 Subject: [PATCH 099/100] Fix card adders popup --- uni/lib/model/providers/startup/session_provider.dart | 7 ------- uni/lib/view/home/widgets/main_cards_list.dart | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 3a91f5a15..96644a1be 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:collection'; import 'package:uni/controller/background_workers/notifications.dart'; import 'package:uni/controller/load_static/terms_and_conditions.dart'; @@ -14,7 +13,6 @@ import 'package:uni/model/request_status.dart'; class SessionProvider extends StateProviderNotifier { late Session _session; - late List _faculties; SessionProvider() : super( @@ -24,9 +22,6 @@ class SessionProvider extends StateProviderNotifier { Session get session => _session; - UnmodifiableListView get faculties => - UnmodifiableListView(_faculties); - @override Future loadFromStorage() async {} @@ -46,8 +41,6 @@ class SessionProvider extends StateProviderNotifier { Future postAuthentication(String username, String password, List faculties, persistentSession) async { - _faculties = faculties; - updateStatus(RequestStatus.busy); Session? session; diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 7871c81d7..72656370f 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -103,12 +103,13 @@ class MainCardsList extends StatelessWidget { } List getCardAdders(BuildContext context) { - final userSession = Provider.of(context, listen: false); + final session = + Provider.of(context, listen: false).session; final List favorites = Provider.of(context, listen: false).favoriteCards; final possibleCardAdditions = cardCreators.entries - .where((e) => e.key.isVisible(userSession.faculties)) + .where((e) => e.key.isVisible(session.faculties)) .where((e) => !favorites.contains(e.key)) .map((e) => Container( decoration: const BoxDecoration(), From 2c0c1b9edb07f136b2d0f7fbf3ade5344b0b9aec Mon Sep 17 00:00:00 2001 From: LuisDuarte1 Date: Thu, 27 Jul 2023 20:10:25 +0000 Subject: [PATCH 100/100] Bump app version [no ci] --- uni/app_version.txt | 2 +- uni/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/app_version.txt b/uni/app_version.txt index de9a1fc68..eb9cb27b0 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.5.42+160 \ No newline at end of file +1.5.43+161 \ No newline at end of file diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index ebf1069a8..d2ae9d87f 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to 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.5.42+160 +version: 1.5.43+161 environment: sdk: ">=2.17.1 <3.0.0"