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 new file mode 100644 index 000000000..636ccc151 --- /dev/null +++ b/.github/workflows/format_lint_test.yaml @@ -0,0 +1,65 @@ +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@v2 + 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 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'zulu' + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Cache pub dependencies + uses: actions/cache@v3 + with: + path: ${{ env.FLUTTER_HOME }}/.pub-cache + key: ${{ runner.os }}-pub-${{ github.ref }}-${{ hashFiles('**/pubspec.lock') }} + restore-keys: ${{ runner.os }}-pub-${{ github.ref }}- + + - run: flutter analyze . + + test: + name: 'Test' + runs-on: ubuntu-latest + needs: lint + defaults: + run: + working-directory: ./uni + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'zulu' + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + + - run: flutter test --no-sound-null-safety diff --git a/.github/workflows/test_lint.yaml b/.github/workflows/test_lint.yaml deleted file mode 100644 index ffb255569..000000000 --- a/.github/workflows/test_lint.yaml +++ /dev/null @@ -1,45 +0,0 @@ -on: pull_request - -jobs: - lint: - name: 'Lint' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./uni - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v1 - with: - java-version: '11.x' - - uses: subosito/flutter-action@v1 - with: - flutter-version: '3.7.2' - - - name: Cache pub dependencies - uses: actions/cache@v2 - with: - path: ${{ env.FLUTTER_HOME }}/.pub-cache - 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 . - - test: - name: 'Test' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./uni - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v1 - with: - java-version: '11.x' - - uses: subosito/flutter-action@v1 - with: - flutter-version: '3.7.2' - - - run: flutter pub get - - run: flutter test --no-sound-null-safety 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). diff --git a/uni/.gitignore b/uni/.gitignore index a48759fe3..9d9b2a895 100644 --- a/uni/.gitignore +++ b/uni/.gitignore @@ -10,6 +10,7 @@ .history .svn/ assets/env/env.json +*.env # IntelliJ related *.iml diff --git a/uni/android/app/build.gradle b/uni/android/app/build.gradle index 930a074cf..1e13ce5bc 100644 --- a/uni/android/app/build.gradle +++ b/uni/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 33 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/android/build.gradle b/uni/android/build.gradle index 96de58432..8dd3ad838 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" } } @@ -26,6 +28,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/app_version.txt b/uni/app_version.txt index 1538c8e29..eb9cb27b0 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.43+161 \ No newline at end of file diff --git a/uni/assets/env/.empty b/uni/assets/env/.empty deleted file mode 100644 index e69de29bb..000000000 diff --git a/uni/assets/env/.env.template b/uni/assets/env/.env.template new file mode 100644 index 000000000..48b37c563 --- /dev/null +++ b/uni/assets/env/.env.template @@ -0,0 +1,3 @@ +## GITHUB TOKEN +## For sending bugs and suggestions to github +GH_TOKEN= \ No newline at end of file diff --git a/uni/assets/images/bus.png b/uni/assets/images/bus.png new file mode 100644 index 000000000..1313c483f Binary files /dev/null and b/uni/assets/images/bus.png differ 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/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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/uni/assets/images/schedule.png b/uni/assets/images/schedule.png new file mode 100644 index 000000000..b409c6d65 Binary files /dev/null and b/uni/assets/images/schedule.png differ diff --git a/uni/assets/images/vacation.png b/uni/assets/images/vacation.png new file mode 100644 index 000000000..b047c93df Binary files /dev/null and b/uni/assets/images/vacation.png differ diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index a270fa644..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 @@ -83,7 +88,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/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 777084d75..db6e43abd 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -45,9 +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)); - _dueDate = DateTime.parse(nextDueDate); + + if (dueDate == null) return false; + + _dueDate = dueDate; return DateTime.now().difference(_dueDate).inDays >= -3; } 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 92% 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..4cb53205e 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,14 +2,14 @@ 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 { 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 new file mode 100644 index 000000000..f2e39788c --- /dev/null +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -0,0 +1,63 @@ +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'; +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) { + return NetworkRouter.getBaseUrlsFromSession(session).toList(); + } + + Future fetchCourseUnitSheet( + Session session, int occurrId) async { + // if course unit is not from the main faculty, Sigarra redirects + 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 { + List courseUnitClasses = []; + + 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 List urls = courseChoiceDocument + .querySelectorAll('a') + .where((element) => + element.attributes['href'] != null && + element.attributes['href']! + .contains('it_listagem.lista_turma_disciplina')) + .map((e) { + String? url = e.attributes['href']!; + if (!url.contains('sigarra.up.pt')) { + url = endpoint + url; + } + return url; + }).toList(); + + for (String url in urls) { + try { + final Response response = + await NetworkRouter.getWithCookies(url, {}, session); + courseUnitClasses += parseCourseUnitClasses(response, endpoint); + } catch (_) { + continue; + } + } + } + + return courseUnitClasses; + } +} 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 78% 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..7160c5369 100644 --- a/uni/lib/controller/fetchers/current_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart @@ -1,23 +1,24 @@ 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_unit.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/session.dart'; 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( - url, {'pv_codigo': session.studentNumber}, session); + final String url = getEndpoints(session)[0]; + final Response response = await NetworkRouter.getWithCookies( + 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 d56ed3347..2e0f50808 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]. @@ -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/exam_fetcher.dart b/uni/lib/controller/fetchers/exam_fetcher.dart index c297eea23..4ddd49076 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/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 new file mode 100644 index 000000000..f1258cc6a --- /dev/null +++ b/uni/lib/controller/fetchers/reference_fetcher.dart @@ -0,0 +1,24 @@ +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/session.dart'; + +class ReferenceFetcher implements SessionDependantFetcher { + @override + List getEndpoints(Session session) { + final List baseUrls = + NetworkRouter.getBaseUrlsFromSession(session) + + [NetworkRouter.getBaseUrl('sasup')]; + final List urls = baseUrls + .map((url) => '${url}gpag_ccorrente_geral.conta_corrente_view') + .toList(); + return urls; + } + + Future getUserReferenceResponse(Session session) { + final List urls = getEndpoints(session); + final String url = urls[0]; + final Map query = {'pct_cod': session.username}; + return NetworkRouter.getWithCookies(url, query, session); + } +} 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/load_info.dart b/uni/lib/controller/load_info.dart deleted file mode 100644 index 93b7c1a56..000000000 --- a/uni/lib/controller/load_info.dart +++ /dev/null @@ -1,145 +0,0 @@ -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 { - 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 loadRemoteUserInfoToState(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); - } - - 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.getUserFees(fees, 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; -} - -void loadLocalUserInfoToState(StateProviders stateProviders, - {skipDatabaseLookup = false}) async { - final Tuple2 userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); - - Logger().i('Setting up user preferences'); - stateProviders.favoriteCardsProvider - .setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); - stateProviders.examProvider.setFilteredExams( - await AppSharedPreferences.getFilteredExams(), Completer()); - stateProviders.examProvider - .setHiddenExams(await AppSharedPreferences.getHiddenExams(), Completer()); - stateProviders.userFacultiesProvider - .setUserFaculties(await AppSharedPreferences.getUserFaculties()); - - 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); -} - -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/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/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/local_storage/app_references_database.dart b/uni/lib/controller/local_storage/app_references_database.dart new file mode 100644 index 000000000..daf8682bf --- /dev/null +++ b/uni/lib/controller/local_storage/app_references_database.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:sqflite/sqflite.dart'; +import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/model/entities/reference.dart'; + +/// Manages the app's References database. +/// +/// This database stores information about the user's references. +/// 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)'''; + + 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 { + await deleteReferences(); + await insertReferences(references); + } + + /// Returns a list containing all the references stored in this database. + Future> references() async { + final Database db = await getDatabase(); + final List> maps = await db.query('refs'); + + return List.generate(maps.length, (i) { + return Reference( + maps[i]['description'], + DateTime.parse(maps[i]['limitDate']), + maps[i]['entity'], + maps[i]['reference'], + maps[i]['amount']); + }); + } + + /// Deletes all of the data in this database. + Future deleteReferences() async { + final Database db = await getDatabase(); + await db.delete('refs'); + } + + /// Adds all items from [references] to this database. + /// + /// 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); + } + } + + /// Migrates [db] from [oldVersion] to [newVersion]. + /// + /// *Note:* This operation only updates the schema of the tables present in + /// the database and, as such, all data is lost. + static FutureOr migrate( + Database db, int oldVersion, int newVersion) async { + final batch = db.batch(); + batch.execute('DROP TABLE IF EXISTS refs'); + batch.execute(createScript); + batch.commit(); + } +} 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_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/controller/local_storage/app_user_database.dart b/uni/lib/controller/local_storage/app_user_database.dart index a8c728bc6..b734359ca 100644 --- a/uni/lib/controller/local_storage/app_user_database.dart +++ b/uni/lib/controller/local_storage/app_user_database.dart @@ -31,13 +31,16 @@ 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 +49,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 +68,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/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/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/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 9899be172..3ad482bd4 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; @@ -7,6 +6,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]. @@ -15,104 +15,99 @@ 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; - static const int loginRequestTimeout = 20; - - static Lock loginLock = Lock(); + /// The timeout for Sigarra login requests. + static const Duration _requestTimeout = Duration(seconds: 10); + + /// The mutual exclusion primitive for login requests. + static final Lock _loginLock = Lock(); + + /// 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 { + 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(_requestTimeout); + + if (response.statusCode != 200) { + Logger().e("Login failed with status code ${response.statusCode}"); + return null; + } - static Function onReloginFail = () {}; + final Session? session = + Session.fromLogin(response, faculties, persistentSession); + if (session == null) { + Logger().e('Login failed: user not authenticated'); + return null; + } - /// 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); - } + }); } - /// Determines if a re-login with the [session] is possible. - static Future relogin(Session session) { - return loginLock.synchronized(() async { - if (!session.persistentSession) { - 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; - if (session.loginRequest != null) { - return session.loginRequest!; - } else { - return session.loginRequest = loginFromSession(session).then((_) { - session.loginRequest = null; - return true; - }); - } - }); + Logger().i('Re-logging in user $username'); + + return await login(username, password, faculties, persistentSession); } - /// 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; - } + /// 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 { + 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(_requestTimeout); + + 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'); - } - + String baseUrl, Map query, Session session, + {Duration timeout = _requestTimeout}) async { if (!baseUrl.contains('?')) { baseUrl += '?'; } @@ -128,37 +123,57 @@ 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(timeout) + : http.get(url.toUri(), headers: headers)) + .timeout(timeout); + if (response.statusCode == 200) { return response; - } else if (response.statusCode == 403 && !(await userLoggedIn(session))) { - // HTTP403 - Forbidden - final bool reLoginSuccessful = await relogin(session); - if (reLoginSuccessful) { + } + + final forbidden = response.statusCode == 403; + 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); + return http.get(url.toUri(), headers: headers).timeout(timeout); } else { - onReloginFail(); - Logger().e('Login failed'); - return Future.error('Login failed'); + // 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(timeout); + return response.statusCode == 200 + ? Future.value(response) + : Future.error('HTTP Error: ${response.statusCode}'); } - } else { - return Future.error('HTTP Error ${response.statusCode}'); } + + 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}'; - 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. @@ -177,16 +192,18 @@ 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)); - if (response.statusCode == 200) { - Logger().i("Logout Successful"); - } else { - Logger().i("Logout Failed"); - } - return response; + 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(_requestTimeout); + + if (response.statusCode == 200) { + Logger().i("Logout Successful"); + } else { + Logger().i("Logout Failed"); + } + + return response; + }); } } diff --git a/uni/lib/controller/on_start_up.dart b/uni/lib/controller/on_start_up.dart deleted file mode 100644 index 48eb205a5..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/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/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart new file mode 100644 index 000000000..483242d1b --- /dev/null +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -0,0 +1,64 @@ +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 { + 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); +} + +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 String className = title.innerHtml.substring( + title.innerHtml.indexOf(' ') + 1, title.innerHtml.indexOf('&')); + + final rows = table?.querySelectorAll('tr'); + if (rows == null || rows.length < 2) { + continue; + } + + final studentRows = rows.sublist(1); + final List students = []; + + 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)); + } + + return classes; +} + +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/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/controller/parsers/parser_fees.dart b/uni/lib/controller/parsers/parser_fees.dart index cd1837dcc..239f5bdf2 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/controller/parsers/parser_references.dart b/uni/lib/controller/parsers/parser_references.dart new file mode 100644 index 000000000..d539b3a80 --- /dev/null +++ b/uni/lib/controller/parsers/parser_references.dart @@ -0,0 +1,31 @@ +import 'package:html/dom.dart'; +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'); + + if (rows.length > 1) { + 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 double amount = double.parse(formattedAmount); + references + .add(Reference(description, limitDate, entity, reference, amount)); + }); + } + + return references; +} 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 new file mode 100644 index 000000000..56f287d9e --- /dev/null +++ b/uni/lib/controller/parsers/parser_session.dart @@ -0,0 +1,7 @@ +import 'package:html/parser.dart'; + +bool isPasswordExpired(String htmlBody) { + final document = parse(htmlBody); + final alerts = document.querySelectorAll('.aviso-invalidado'); + return alerts.length >= 2 && alerts[1].text.contains('expirad'); +} diff --git a/uni/lib/main.dart b/uni/lib/main.dart index fa5edf566..785cb030b 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -3,25 +3,25 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:logger/logger.dart'; 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/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/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/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/course_units_info_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/reference_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/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'; @@ -31,10 +31,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'; @@ -53,23 +53,27 @@ Future main() async { ExamProvider(), BusStopProvider(), RestaurantProvider(), - ProfileStateProvider(), + ProfileProvider(), + CourseUnitsInfoProvider(), SessionProvider(), CalendarProvider(), LibraryOccupationProvider(), FacultyLocationsProvider(), - LastUserInfoProvider(), - UserFacultiesProvider(), - FavoriteCardsProvider(), - HomePageEditingModeProvider()); + HomePageProvider(), + ReferenceProvider()); - 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 + ); + await dotenv + .load(fileName: "assets/env/.env", isOptional: true) + .onError((error, stackTrace) { + Logger().e("Error loading .env file: $error", error, stackTrace); + }); final savedTheme = await AppSharedPreferences.getThemeMode(); await SentryFlutter.init((options) { @@ -88,7 +92,10 @@ Future main() async { ChangeNotifierProvider( create: (context) => stateProviders.restaurantProvider), ChangeNotifierProvider( - create: (context) => stateProviders.profileStateProvider), + create: (context) => stateProviders.profileProvider), + ChangeNotifierProvider( + create: (context) => + stateProviders.courseUnitsInfoProvider), ChangeNotifierProvider( create: (context) => stateProviders.sessionProvider), ChangeNotifierProvider( @@ -100,15 +107,9 @@ Future main() async { create: (context) => stateProviders.facultyLocationsProvider), ChangeNotifierProvider( - create: (context) => stateProviders.lastUserInfoProvider), - ChangeNotifierProvider( - create: (context) => - stateProviders.userFacultiesProvider), - ChangeNotifierProvider( - create: (context) => - stateProviders.favoriteCardsProvider), + create: (context) => stateProviders.homePageProvider), ChangeNotifierProvider( - create: (context) => stateProviders.homePageEditingMode), + create: (context) => stateProviders.referenceProvider), ], child: ChangeNotifierProvider( create: (_) => ThemeNotifier(savedTheme), @@ -135,6 +136,7 @@ class MyAppState extends State { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); + return Consumer( builder: (context, themeNotifier, _) => MaterialApp( title: 'uni', @@ -186,7 +188,6 @@ class MyAppState extends State { maintainState: false), '/${DrawerItem.navLogOut.title}': LogoutRoute.buildLogoutRoute() }; - return transitions[settings.name]; }), ); 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/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..d1948287f --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_class.dart @@ -0,0 +1,16 @@ +class CourseUnitClass { + String className; + List students; + CourseUnitClass(this.className, this.students); +} + +class CourseUnitStudent { + String name; + int number; + String mail; + Uri photo; + Uri profile; + + CourseUnitStudent( + this.name, this.number, this.mail, this.photo, this.profile); +} 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/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/login_exceptions.dart b/uni/lib/model/entities/login_exceptions.dart new file mode 100644 index 000000000..908a96dd0 --- /dev/null +++ b/uni/lib/model/entities/login_exceptions.dart @@ -0,0 +1,13 @@ +class ExpiredCredentialsException implements Exception { + ExpiredCredentialsException(); +} + +class InternetStatusException implements Exception { + String message = 'Verifica a tua ligação à internet'; + InternetStatusException(); +} + +class WrongCredentialsException implements Exception { + String message = 'Credenciais inválidas'; + WrongCredentialsException(); +} diff --git a/uni/lib/model/entities/profile.dart b/uni/lib/model/entities/profile.dart index 6c222c657..be5bdf835 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_units/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; + final DateTime? feesLimit; + List courses; + List courseUnits; Profile( {this.name = '', @@ -18,8 +20,9 @@ class Profile { courses, this.printBalance = '', this.feesBalance = '', - this.feesLimit = ''}) - : courses = courses ?? []; + this.feesLimit}) + : courses = courses ?? [], + courseUnits = []; /// Creates a new instance from a JSON object. static Profile fromResponse(dynamic response) { diff --git a/uni/lib/model/entities/reference.dart b/uni/lib/model/entities/reference.dart new file mode 100644 index 000000000..63a249845 --- /dev/null +++ b/uni/lib/model/entities/reference.dart @@ -0,0 +1,21 @@ +class Reference { + final String description; + final DateTime limitDate; + final int entity; + final int reference; + final double amount; + + Reference(this.description, this.limitDate, this.entity, this.reference, + this.amount); + + /// Converts this reference to a map. + Map toMap() { + return { + 'description': description, + 'limitDate': limitDate.toString(), + 'entity': entity, + 'reference': reference, + 'amount': amount, + }; + } +} diff --git a/uni/lib/model/entities/session.dart b/uni/lib/model/entities/session.dart index a78e9810e..7108ddd89 100644 --- a/uni/lib/model/entities/session.dart +++ b/uni/lib/model/entities/session.dart @@ -1,47 +1,37 @@ 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 [''], - this.persistentSession = false}); + {required this.username, + required this.cookies, + required this.faculties, + this.persistentSession = false}) { + assert(faculties.isNotEmpty); + } - /// 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/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/faculty_locations_provider.dart b/uni/lib/model/providers/faculty_locations_provider.dart deleted file mode 100644 index 0b3ee6478..000000000 --- a/uni/lib/model/providers/faculty_locations_provider.dart +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -class FacultyLocationsProvider extends StateProviderNotifier { - List _locations = []; - - 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(); - } -} diff --git a/uni/lib/model/providers/favorite_cards_provider.dart b/uni/lib/model/providers/favorite_cards_provider.dart deleted file mode 100644 index 99ef9088c..000000000 --- a/uni/lib/model/providers/favorite_cards_provider.dart +++ /dev/null @@ -1,13 +0,0 @@ -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(); - - 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 deleted file mode 100644 index e4506f86d..000000000 --- a/uni/lib/model/providers/home_page_editing_mode_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:uni/model/providers/state_provider_notifier.dart'; - -class HomePageEditingModeProvider extends StateProviderNotifier { - bool _isEditing = false; - - bool get isEditing => _isEditing; - - setHomePageEditingMode(bool state) { - _isEditing = state; - notifyListeners(); - } - - toggle() { - _isEditing = !_isEditing; - notifyListeners(); - } -} 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 f9774d35c..000000000 --- a/uni/lib/model/providers/last_user_info_provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:async'; - -import 'package:uni/controller/local_storage/app_last_user_info_update_database.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(); - } - - updateStateBasedOnLocalTime() async { - final AppLastUserInfoUpdateDatabase db = AppLastUserInfoUpdateDatabase(); - _lastUpdateTime = await db.getLastUserInfoUpdateTime(); - notifyListeners(); - } -} diff --git a/uni/lib/model/providers/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart similarity index 76% rename from uni/lib/model/providers/bus_stop_provider.dart rename to uni/lib/model/providers/lazy/bus_stop_provider.dart index 202b6974b..50e478295 100644 --- a/uni/lib/model/providers/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -4,23 +4,37 @@ 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/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'; class BusStopProvider extends StateProviderNotifier { Map _configuredBusStops = Map.identity(); DateTime _timeStamp = DateTime.now(); + BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); + UnmodifiableMapView get configuredBusStops => UnmodifiableMapView(_configuredBusStops); DateTime get timeStamp => _timeStamp; - getUserBusTrips(Completer action) async { - updateStatus(RequestStatus.busy); + @override + Future loadFromStorage() async { + final AppBusStopDatabase busStopsDb = AppBusStopDatabase(); + final Map stops = await busStopsDb.busStops(); + _configuredBusStops = stops; + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchUserBusTrips(); + } + Future fetchUserBusTrips() async { try { for (String stopCode in configuredBusStops.keys) { final List stopTrips = @@ -34,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)) { @@ -51,41 +62,32 @@ 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); } - - 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/lazy/calendar_provider.dart similarity index 72% rename from uni/lib/model/providers/calendar_provider.dart rename to uni/lib/model/providers/lazy/calendar_provider.dart index 4bcbea2b5..4776f6bcf 100644 --- a/uni/lib/model/providers/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -1,40 +1,44 @@ 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/request_status.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'; class CalendarProvider extends StateProviderNotifier { List _calendar = []; + CalendarProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 30)); + UnmodifiableListView get calendar => UnmodifiableListView(_calendar); - getCalendarFromFetcher(Session session, Completer action) async { - try { - updateStatus(RequestStatus.busy); + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchCalendar(session); + } + Future fetchCalendar(Session session) async { + try { _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(); } - updateStateBasedOnLocalCalendar() async { + @override + Future loadFromStorage() async { final CalendarDatabase db = CalendarDatabase(); _calendar = await db.calendar(); - notifyListeners(); } } diff --git a/uni/lib/model/providers/lazy/course_units_info_provider.dart b/uni/lib/model/providers/lazy/course_units_info_provider.dart new file mode 100644 index 000000000..8b3851264 --- /dev/null +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -0,0 +1,60 @@ +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/profile.dart'; +import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/request_status.dart'; + +class CourseUnitsInfoProvider extends StateProviderNotifier { + final Map _courseUnitsSheets = {}; + final Map> _courseUnitsClasses = {}; + + CourseUnitsInfoProvider() + : super(dependsOnSession: true, cacheDuration: null, initialize: false); + + UnmodifiableMapView get courseUnitsSheets => + UnmodifiableMapView(_courseUnitsSheets); + + UnmodifiableMapView> + get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); + + fetchCourseUnitSheet(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); + } + + fetchCourseUnitClasses(CourseUnit courseUnit, Session session) async { + updateStatus(RequestStatus.busy); + try { + _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() + .fetchCourseUnitClasses(session, courseUnit.occurrId); + } catch (e) { + updateStatus(RequestStatus.failed); + Logger() + .e('Failed to get course unit classes for ${courseUnit.name}: $e'); + return; + } + updateStatus(RequestStatus.successful); + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + // Course units info is loaded on demand by its detail page + } + + @override + Future loadFromStorage() async {} +} diff --git a/uni/lib/model/providers/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart similarity index 71% rename from uni/lib/model/providers/exam_provider.dart rename to uni/lib/model/providers/lazy/exam_provider.dart index 2ba2bc8d3..161573f08 100644 --- a/uni/lib/model/providers/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -1,25 +1,25 @@ 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'; 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'; 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 = {}; - + ExamProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); UnmodifiableListView get exams => UnmodifiableListView(_exams); @@ -29,8 +29,27 @@ class ExamProvider extends StateProviderNotifier { UnmodifiableMapView get filteredExamsTypes => UnmodifiableMapView(_filteredExamsTypes); - void getUserExams( - Completer action, + @override + Future loadFromStorage() async { + await setFilteredExams(await AppSharedPreferences.getFilteredExams()); + await setHiddenExams(await AppSharedPreferences.getHiddenExams()); + + final AppExamsDatabase db = AppExamsDatabase(); + final List exams = await db.exams(); + _exams = exams; + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchUserExams( + ParserExams(), + await AppSharedPreferences.getPersistentUserInfo(), + profile, + session, + profile.courseUnits); + } + + Future fetchUserExams( ParserExams parserExams, Tuple2 userPersistentInfo, Profile profile, @@ -38,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); @@ -54,20 +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(); - } - - updateStateBasedOnLocalUserExams() async { - final AppExamsDatabase db = AppExamsDatabase(); - final List exs = await db.exams(); - _exams = exs; - notifyListeners(); } updateFilteredExams() async { @@ -76,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(); } @@ -91,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/model/providers/lazy/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart new file mode 100644 index 000000000..3f210e468 --- /dev/null +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -0,0 +1,30 @@ +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 'package:uni/model/request_status.dart'; + +class FacultyLocationsProvider extends StateProviderNotifier { + List _locations = []; + + FacultyLocationsProvider() + : super(dependsOnSession: false, cacheDuration: const Duration(days: 30)); + + UnmodifiableListView get locations => + UnmodifiableListView(_locations); + + @override + Future loadFromStorage() async { + updateStatus(RequestStatus.busy); + _locations = await LocationFetcherAsset().getLocations(); + updateStatus(RequestStatus.successful); + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + updateStatus(RequestStatus.successful); + } +} diff --git a/uni/lib/model/providers/lazy/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart new file mode 100644 index 000000000..31cfa6130 --- /dev/null +++ b/uni/lib/model/providers/lazy/home_page_provider.dart @@ -0,0 +1,42 @@ +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/request_status.dart'; +import 'package:uni/utils/favorite_widget_type.dart'; + +class HomePageProvider extends StateProviderNotifier { + List _favoriteCards = []; + bool _isEditing = false; + + HomePageProvider() : super(dependsOnSession: false, cacheDuration: null); + + List get favoriteCards => _favoriteCards.toList(); + + bool get isEditing => _isEditing; + + @override + Future loadFromStorage() async { + setFavoriteCards(await AppSharedPreferences.getFavoriteCards()); + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + updateStatus(RequestStatus.successful); + } + + setHomePageEditingMode(bool state) { + _isEditing = state; + notifyListeners(); + } + + toggleHomePageEditingMode() { + _isEditing = !_isEditing; + notifyListeners(); + } + + setFavoriteCards(List favoriteCards) { + _favoriteCards = favoriteCards; + notifyListeners(); + } +} diff --git a/uni/lib/model/providers/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart similarity index 74% rename from uni/lib/model/providers/lecture_provider.dart rename to uni/lib/model/providers/lazy/lecture_provider.dart index 12c415908..d8fa13a1e 100644 --- a/uni/lib/model/providers/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -1,49 +1,56 @@ 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'; 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/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'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/request_status.dart'; class LectureProvider extends StateProviderNotifier { List _lectures = []; + LectureProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 6)); + UnmodifiableListView get lectures => UnmodifiableListView(_lectures); - void getUserLectures( - Completer action, - Tuple2 userPersistentInfo, - Session session, - Profile profile, + @override + Future loadFromStorage() async { + final AppLecturesDatabase db = AppLecturesDatabase(); + final List lectures = await db.lectures(); + _lectures = lectures; + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchUserLectures( + await AppSharedPreferences.getPersistentUserInfo(), session, profile); + } + + Future fetchUserLectures(Tuple2 userPersistentInfo, + Session session, Profile profile, {ScheduleFetcher? fetcher}) async { try { - updateStatus(RequestStatus.busy); - final List lectures = 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( @@ -55,11 +62,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/lazy/library_occupation_provider.dart similarity index 61% rename from uni/lib/model/providers/library_occupation_provider.dart rename to uni/lib/model/providers/lazy/library_occupation_provider.dart index 038cb8ffc..26a1dbeb5 100644 --- a/uni/lib/model/providers/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -1,9 +1,9 @@ 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'; +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'; @@ -11,37 +11,34 @@ import 'package:uni/model/request_status.dart'; class LibraryOccupationProvider extends StateProviderNotifier { LibraryOccupation? _occupation; + LibraryOccupationProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); + LibraryOccupation? get occupation => _occupation; - void getLibraryOccupation( - Session session, - Completer action, - ) async { - try { - updateStatus(RequestStatus.busy); + @override + Future loadFromStorage() async { + final LibraryOccupationDatabase db = LibraryOccupationDatabase(); + final LibraryOccupation occupation = await db.occupation(); + _occupation = occupation; + } - final LibraryOccupation occupation = - await LibraryOccupationFetcherSheets() - .getLibraryOccupationFromSheets(session); + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchLibraryOccupation(session); + } + + Future fetchLibraryOccupation(Session session) async { + try { + _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(); - } - - Future updateStateBasedOnLocalOccupation() async { - final LibraryOccupationDatabase db = LibraryOccupationDatabase(); - final LibraryOccupation occupation = await db.occupation(); - - _occupation = occupation; - notifyListeners(); } } diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart new file mode 100644 index 000000000..829658416 --- /dev/null +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -0,0 +1,49 @@ +import 'dart:async'; +import 'dart:collection'; + +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'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/entities/reference.dart'; +import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/request_status.dart'; + +class ReferenceProvider extends StateProviderNotifier { + List _references = []; + + ReferenceProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); + + UnmodifiableListView get references => + UnmodifiableListView(_references); + + @override + Future loadFromStorage() async { + final AppReferencesDatabase referencesDb = AppReferencesDatabase(); + final List references = await referencesDb.references(); + _references = references; + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchUserReferences(session); + } + + Future fetchUserReferences(Session session) async { + try { + final response = + await ReferenceFetcher().getUserReferenceResponse(session); + + _references = await parseReferences(response); + + updateStatus(RequestStatus.successful); + + final referencesDb = AppReferencesDatabase(); + referencesDb.saveNewReferences(references); + } catch (e) { + updateStatus(RequestStatus.failed); + } + } +} diff --git a/uni/lib/model/providers/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart similarity index 72% rename from uni/lib/model/providers/restaurant_provider.dart rename to uni/lib/model/providers/lazy/restaurant_provider.dart index 54765609d..9634de134 100644 --- a/uni/lib/model/providers/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -1,46 +1,48 @@ 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'; 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 = []; + RestaurantProvider() + : super(dependsOnSession: false, cacheDuration: const Duration(days: 1)); + UnmodifiableListView get restaurants => UnmodifiableListView(_restaurants); - void getRestaurantsFromFetcher( - Completer action, Session session) async { - try { - updateStatus(RequestStatus.busy); + @override + Future loadFromStorage() async { + final RestaurantDatabase restaurantDb = RestaurantDatabase(); + final List restaurants = await restaurantDb.getRestaurants(); + _restaurants = restaurants; + } + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchRestaurants(session); + } + + Future fetchRestaurants(Session session) async { + try { final List restaurants = await RestaurantFetcher().getRestaurants(session); - // Updates local database according to information fetched -- Restaurants + 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(); - } - - 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 deleted file mode 100644 index 4617aadac..000000000 --- a/uni/lib/model/providers/session_provider.dart +++ /dev/null @@ -1,106 +0,0 @@ -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'; -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'; - -class SessionProvider extends StateProviderNotifier { - Session _session = Session(); - List _faculties = []; - - Session get session => _session; - - UnmodifiableListView get faculties => - UnmodifiableListView(_faculties); - - login( - Completer action, - String username, - String password, - List faculties, - StateProviders stateProviders, - persistentSession, - usernameController, - passwordController) 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() - }); - - loadLocalUserInfoToState(stateProviders, skipDatabaseLookup: true); - await loadRemoteUserInfoToState(stateProviders); - - usernameController.clear(); - passwordController.clear(); - await acceptTermsAndConditions(); - - updateStatus(RequestStatus.successful); - } else { - updateStatus(RequestStatus.failed); - } - } catch (e) { - updateStatus(RequestStatus.failed); - } - - notifyListeners(); - action.complete(); - } - - 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 loadRemoteUserInfoToState(stateProviders); - Future.delayed(const Duration(seconds: 20), ()=>{ - NotificationManager().initializeNotifications() - }); - updateStatus(RequestStatus.successful); - action?.complete(); - } else { - failRelogin(action); - } - } catch (e) { - _session = Session( - studentNumber: username, - authenticated: false, - faculties: faculties, - type: '', - cookies: '', - persistentSession: true); - - failRelogin(action); - } - } - - void failRelogin(Completer? action) { - notifyListeners(); - updateStatus(RequestStatus.failed); - action?.completeError(RequestStatus.failed); - NetworkRouter.onReloginFail(); - } -} diff --git a/uni/lib/model/providers/profile_state_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart similarity index 63% rename from uni/lib/model/providers/profile_state_provider.dart rename to uni/lib/model/providers/startup/profile_provider.dart index 619b7a6de..f28acbb93 100644 --- a/uni/lib/model/providers/profile_state_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:collection'; +import 'dart:io'; -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'; @@ -12,26 +12,23 @@ 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/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'; +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); + ProfileProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); String get feesRefreshTime => _feesRefreshTime.toString(); @@ -39,41 +36,73 @@ class ProfileStateProvider extends StateProviderNotifier { Profile get profile => _profile; - updateStateBasedOnLocalProfile() async { + @override + Future loadFromStorage() async { + await loadProfile(); + await Future.wait( + [loadCourses(), loadBalanceRefreshTimes(), loadCourseUnits()]); + } + + @override + Future loadFromRemote(Session session, Profile profile) async { + await fetchUserInfo(session); + + await Future.wait([ + fetchUserFees(session), + fetchUserPrintBalance(session), + fetchCourseUnitsAndCourseAverages(session) + ]); + + if (status != RequestStatus.failed) { + updateStatus(RequestStatus.successful); + } + } + + Future loadProfile() async { final profileDb = AppUserDataDatabase(); - final Profile profile = await profileDb.getUserData(); + _profile = await profileDb.getUserData(); + } + Future loadCourses() async { final AppCoursesDatabase coursesDb = AppCoursesDatabase(); final List courses = await coursesDb.courses(); + _profile.courses = courses; + } - profile.courses = courses; + Future 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(); + Future loadCourseUnits() async { + final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); + profile.courseUnits = await db.courseUnits(); } - getUserFees(Completer action, Session session) async { + Future fetchUserFees(Session session) async { try { 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 = await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('fees', currentTime.toString()); - - // Store fees info final profileDb = AppUserDataDatabase(); - profileDb.saveUserFees(Tuple2(feesBalance, feesLimit)); + profileDb.saveUserFees(feesBalance, feesLimit); } final Profile newProfile = Profile( @@ -86,12 +115,9 @@ class ProfileStateProvider 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 { @@ -100,7 +126,7 @@ class ProfileStateProvider extends StateProviderNotifier { refreshTimesDatabase.saveRefreshTime(db, currentTime); } - getUserPrintBalance(Completer action, Session session) async { + Future fetchUserPrintBalance(Session session) async { try { final response = await PrintFetcher().getUserPrintsResponse(session); final String printBalance = await getPrintsBalance(response); @@ -110,8 +136,6 @@ class ProfileStateProvider extends StateProviderNotifier { await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { await storeRefreshTime('print', currentTime.toString()); - - // Store fees info final profileDb = AppUserDataDatabase(); profileDb.saveUserPrintBalance(printBalance); } @@ -126,42 +150,20 @@ class ProfileStateProvider extends StateProviderNotifier { _profile = newProfile; _printRefreshTime = currentTime; - notifyListeners(); } catch (e) { - Logger().e('Failed to get Print Balance'); - } - - 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); + updateStatus(RequestStatus.failed); } } - getUserInfo(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); - final profile = ProfileFetcher.getProfile(session).then((res) { - _profile = res; - }); + _profile = profile; + _profile.courseUnits = currentCourseUnits; - final ucs = CurrentCourseUnitsFetcher() - .getCurrentCourseUnits(session) - .then((res) => _currUcs = res); - await Future.wait([profile, ucs]); - notifyListeners(); updateStatus(RequestStatus.successful); final Tuple2 userPersistentInfo = @@ -171,22 +173,17 @@ class ProfileStateProvider extends StateProviderNotifier { profileDb.insertUserData(_profile); } } catch (e) { - Logger().e('Failed to get User Info'); updateStatus(RequestStatus.failed); } - - action.complete(); } - getCourseUnitsAndCourseAverages( - Session session, Completer action) async { - updateStatus(RequestStatus.busy); + Future fetchCourseUnitsAndCourseAverages(Session session) async { try { final List courses = profile.courses; - _currUcs = await AllCourseUnitsFetcher() - .getAllCourseUnitsAndCourseAverages(courses, session); - updateStatus(RequestStatus.successful); - notifyListeners(); + final List allCourseUnits = await AllCourseUnitsFetcher() + .getAllCourseUnitsAndCourseAverages(profile.courses, session); + + _profile.courseUnits = allCourseUnits; final Tuple2 userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); @@ -195,19 +192,25 @@ class ProfileStateProvider extends StateProviderNotifier { await coursesDb.saveNewCourses(courses); final courseUnitsDatabase = AppCourseUnitsDatabase(); - await courseUnitsDatabase.saveNewCourseUnits(currUcs); + await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); } } catch (e) { - Logger().e('Failed to get all user ucs: $e'); updateStatus(RequestStatus.failed); } - - action.complete(); } - updateStateBasedOnLocalCourseUnits() async { - final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); - _currUcs = await db.courseUnits(); - notifyListeners(); + static Future fetchOrGetCachedProfilePicture( + int? studentNumber, Session session, + {forceRetrieval = false}) { + studentNumber ??= int.parse(session.username.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( + '${studentNumber}_profile_picture', url, headers, + forceRetrieval: forceRetrieval); } } diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart new file mode 100644 index 000000000..96644a1be --- /dev/null +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:uni/controller/background_workers/notifications.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'; +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/request_status.dart'; + +class SessionProvider extends StateProviderNotifier { + late Session _session; + + SessionProvider() + : super( + dependsOnSession: false, + cacheDuration: null, + initialStatus: RequestStatus.none); + + Session get session => _session; + + @override + Future loadFromStorage() async {} + + @override + Future loadFromRemote(Session session, Profile profile) async { + updateStatus(RequestStatus.successful); + } + + void restoreSession( + String username, String password, List faculties) { + _session = Session( + faculties: faculties, + username: username, + cookies: "", + persistentSession: true); + } + + Future postAuthentication(String username, String password, + List faculties, persistentSession) async { + updateStatus(RequestStatus.busy); + + Session? session; + try { + session = await NetworkRouter.login( + username, password, faculties, persistentSession); + } catch (e) { + updateStatus(RequestStatus.failed); + throw InternetStatusException(); + } + + if (session == null) { + final String responseHtml = + await NetworkRouter.loginInSigarra(username, password, faculties); + + updateStatus(RequestStatus.failed); + + if (isPasswordExpired(responseHtml)) { + throw ExpiredCredentialsException(); + } else { + throw WrongCredentialsException(); + } + } + + _session = session; + + if (persistentSession) { + await AppSharedPreferences.savePersistentUserInfo( + username, password, faculties); + } + + Future.delayed(const Duration(seconds: 20), + () => {NotificationManager().initializeNotifications()}); + + await acceptTermsAndConditions(); + updateStatus(RequestStatus.successful); + } +} diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 2e602f197..5d8acc1b7 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,14 +1,158 @@ -import 'package:flutter/cupertino.dart'; - +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'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/entities/session.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 { - RequestStatus _status = RequestStatus.none; + static final Lock _lock = Lock(); + RequestStatus _status; + bool _initializedFromStorage; + bool _initializedFromRemote; + DateTime? _lastUpdateTime; + bool dependsOnSession; + Duration? cacheDuration; RequestStatus get status => _status; + DateTime? get lastUpdateTime => _lastUpdateTime; + + StateProviderNotifier( + {required this.dependsOnSession, + required this.cacheDuration, + RequestStatus initialStatus = RequestStatus.busy, + bool initialize = true}) + : _status = initialStatus, + _initializedFromStorage = !initialize, + _initializedFromRemote = !initialize; + + Future _loadFromStorage() async { + _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( + runtimeType.toString()); + + await loadFromStorage(); + notifyListeners(); + Logger().i("Loaded $runtimeType info from storage"); + } + + Future _loadFromRemote(Session session, Profile profile, + {bool force = false}) async { + final shouldReload = force || + _lastUpdateTime == null || + cacheDuration == null || + DateTime.now().difference(_lastUpdateTime!) > cacheDuration!; + + if (!shouldReload) { + Logger().i( + "Last info for $runtimeType is within cache period (last updated on $_lastUpdateTime); " + "skipping remote load"); + updateStatus(RequestStatus.successful); + return; + } + + final bool hasConnectivity = + await Connectivity().checkConnectivity() != ConnectivityResult.none; + + if (!hasConnectivity) { + Logger().w("No internet connection; skipping $runtimeType remote load"); + updateStatus(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"); + } + } + void updateStatus(RequestStatus status) { _status = status; notifyListeners(); } + + Future forceRefresh(BuildContext context) async { + 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; + } + + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; + + _loadFromRemote(session, profile, force: true); + }); + } + + Future ensureInitialized(BuildContext context) async { + await ensureInitializedFromStorage(); + + if (context.mounted) { + await ensureInitializedFromRemote(context); + } + } + + Future ensureInitializedFromRemote(BuildContext context) async { + await _lock.synchronized(() async { + if (_initializedFromRemote) { + return; + } + + _initializedFromRemote = true; + + final session = + Provider.of(context, listen: false).session; + final profile = + Provider.of(context, listen: false).profile; + + await _loadFromRemote(session, profile); + }); + } + + /// 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 ensureInitializedFromStorage() async { + await _lock.synchronized(() async { + if (_initializedFromStorage) { + return; + } + + _initializedFromStorage = true; + await _loadFromStorage(); + }); + } + + 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/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index a52545ccd..b464a4e9b 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -1,48 +1,45 @@ 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/favorite_cards_provider.dart'; -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/restaurant_provider.dart'; -import 'package:uni/model/providers/session_provider.dart'; -import 'package:uni/model/providers/user_faculties_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/course_units_info_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/reference_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; final ExamProvider examProvider; final BusStopProvider busStopProvider; final RestaurantProvider restaurantProvider; - final ProfileStateProvider profileStateProvider; + final CourseUnitsInfoProvider courseUnitsInfoProvider; + final ProfileProvider profileProvider; final SessionProvider sessionProvider; final CalendarProvider calendarProvider; final LibraryOccupationProvider libraryOccupationProvider; final FacultyLocationsProvider facultyLocationsProvider; - final LastUserInfoProvider lastUserInfoProvider; - final UserFacultiesProvider userFacultiesProvider; - final FavoriteCardsProvider favoriteCardsProvider; - final HomePageEditingModeProvider homePageEditingMode; + final HomePageProvider homePageProvider; + final ReferenceProvider referenceProvider; StateProviders( this.lectureProvider, this.examProvider, this.busStopProvider, this.restaurantProvider, - this.profileStateProvider, + this.profileProvider, + this.courseUnitsInfoProvider, this.sessionProvider, this.calendarProvider, this.libraryOccupationProvider, this.facultyLocationsProvider, - this.lastUserInfoProvider, - this.userFacultiesProvider, - this.favoriteCardsProvider, - this.homePageEditingMode); + this.homePageProvider, + this.referenceProvider); static StateProviders fromContext(BuildContext context) { final lectureProvider = @@ -52,8 +49,10 @@ class StateProviders { Provider.of(context, listen: false); final restaurantProvider = Provider.of(context, listen: false); - final profileStateProvider = - Provider.of(context, listen: false); + final courseUnitsInfoProvider = + Provider.of(context, listen: false); + final profileProvider = + Provider.of(context, listen: false); final sessionProvider = Provider.of(context, listen: false); final calendarProvider = @@ -62,28 +61,23 @@ class StateProviders { Provider.of(context, listen: false); final facultyLocationsProvider = 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 = - Provider.of(context, listen: false); + final homePageProvider = + Provider.of(context, listen: false); + final referenceProvider = + Provider.of(context, listen: false); return StateProviders( lectureProvider, examProvider, busStopProvider, restaurantProvider, - profileStateProvider, + profileProvider, + courseUnitsInfoProvider, sessionProvider, calendarProvider, libraryOccupationProvider, facultyLocationsProvider, - lastUserInfoProvider, - userFacultiesProvider, - favoriteCardsProvider, - homePageEditingMode); + homePageProvider, + referenceProvider); } } 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 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/about/about.dart b/uni/lib/view/about/about.dart index 411a1901d..1c112c7c9 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}); @@ -17,25 +17,22 @@ class AboutPageViewState extends GeneralPageViewState { final MediaQueryData queryData = MediaQuery.of(context); return ListView( children: [ - SvgPicture.asset( - 'assets/images/ni_logo.svg', - colorFilter: - ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.srcIn), - width: queryData.size.height / 7, - height: queryData.size.height / 7, - ), + Padding( + padding: EdgeInsets.only(top: queryData.size.width / 12), + child: SvgPicture.asset( + 'assets/images/logo_ni.svg', + width: queryData.size.height / 7, + height: queryData.size.height / 7, + )), Center( child: Padding( - padding: EdgeInsets.only( - left: queryData.size.width / 12, - right: queryData.size.width / 12, - top: queryData.size.width / 12, - bottom: queryData.size.width / 12), - child: Column(children: const [ - TermsAndConditions(), - ]), + padding: EdgeInsets.all(queryData.size.width / 12), + child: const TermsAndConditions(), )) ], ); } + + @override + Future onRefresh(BuildContext context) async {} } 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/bug_report/bug_report.dart b/uni/lib/view/bug_report/bug_report.dart index 6a263d31a..eb45bdfa7 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 onRefresh(BuildContext context) async {} } diff --git a/uni/lib/view/bug_report/widgets/form.dart b/uni/lib/view/bug_report/widgets/form.dart index f63d5b9fb..250a1d75b 100644 --- a/uni/lib/view/bug_report/widgets/form.dart +++ b/uni/lib/view/bug_report/widgets/form.dart @@ -2,16 +2,16 @@ import 'dart:convert'; import 'package:email_validator/email_validator.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:logger/logger.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:tuple/tuple.dart'; +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/entities/bug_report.dart'; import 'package:uni/view/bug_report/widgets/text_field.dart'; import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; class BugReportForm extends StatefulWidget { const BugReportForm({super.key}); @@ -46,15 +46,14 @@ class BugReportFormState extends State { static final TextEditingController descriptionController = TextEditingController(); static final TextEditingController emailController = TextEditingController(); - String ghToken = ''; bool _isButtonTapped = false; bool _isConsentGiven = false; BugReportFormState() { - if (ghToken == '') loadGHKey(); loadBugClassList(); } + void loadBugClassList() { bugList = []; @@ -289,7 +288,7 @@ class BugReportFormState extends State { .post(Uri.parse(_gitHubPostUrl), headers: { 'Content-Type': 'application/json', - 'Authorization': 'token $ghToken' + 'Authorization': 'token ${dotenv.env["GH_TOKEN"]}}' }, body: json.encode(data)) .then((http.Response response) { @@ -316,16 +315,4 @@ class BugReportFormState extends State { _isConsentGiven = false; }); } - - Future> parseJsonFromAssets(String assetsPath) async { - return rootBundle - .loadString(assetsPath) - .then((jsonStr) => jsonDecode(jsonStr)); - } - - void loadGHKey() async { - final Map dataMap = - await parseJsonFromAssets('assets/env/env.json'); - ghToken = dataMap['gh_token']; - } } 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..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 @@ -1,13 +1,15 @@ 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/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'; 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,12 +23,17 @@ class BusStopNextArrivalsPageState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, busProvider, _) => ListView(children: [ - NextArrivals( - busProvider.configuredBusStops, busProvider.status) + return LazyConsumer( + builder: (context, busProvider) => ListView(children: [ + NextArrivals(busProvider.configuredBusStops, busProvider.status) ])); } + + @override + Future onRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } class NextArrivals extends StatefulWidget { @@ -34,8 +41,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,36 +52,30 @@ 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 + List requestSuccessful(context) { final List result = []; @@ -84,8 +84,22 @@ 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; @@ -135,7 +149,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_next_arrivals/widgets/estimated_arrival_timestamp.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart index c1ec2d134..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,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/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 class EstimatedArrivalTimeStamp extends StatelessWidget { @@ -13,8 +13,8 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, busProvider, _) => + 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..21c7b6dd1 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -1,12 +1,12 @@ 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'; +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'; 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))); @@ -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), @@ -72,4 +72,7 @@ class BusStopSelectionPageState ]); }); } + + @override + Future onRefresh(BuildContext context) async {} } 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..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 @@ -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 @@ -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 6c26e7fe8..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 @@ -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 { @@ -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 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 4942cc005..ffd8791b7 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -2,9 +2,11 @@ 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'; +import 'package:uni/view/lazy_consumer.dart'; class CalendarPageView extends StatefulWidget { const CalendarPageView({Key? key}) : super(key: key); @@ -16,19 +18,20 @@ class CalendarPageView extends StatefulWidget { class CalendarPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, calendarProvider, _) => - getCalendarPage(context, calendarProvider.calendar), - ); - } - - Widget getCalendarPage(BuildContext context, List calendar) { - return ListView( - children: [_getPageTitle(), getTimeline(context, calendar)]); + return LazyConsumer( + 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), @@ -66,4 +69,10 @@ class CalendarPageViewState extends GeneralPageViewState { ), ); } + + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } 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..f4a60e0d0 --- /dev/null +++ b/uni/lib/view/common_widgets/expanded_image_label.dart @@ -0,0 +1,42 @@ +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, + ), + ], + ); + } +} diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index dc92d9d04..d85ef74a1 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 @@ -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, @@ -104,7 +106,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/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index f7429d9dd..d26f3a631 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -1,24 +1,28 @@ -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 expandable 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); + + 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( - 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,7 +30,7 @@ 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(), + title: Text(getTitle(), style: Theme.of(context) .textTheme .headlineSmall @@ -35,7 +39,7 @@ class GenericExpansionCardState extends State { 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/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 4b5138ce2..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:provider/provider.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 Consumer( - 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/pages_layouts/general/general.dart b/uni/lib/view/common_widgets/pages_layouts/general/general.dart index ad8f9f20e..1646fe4ba 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -4,9 +4,8 @@ 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/session_provider.dart'; -import 'package:uni/model/providers/state_providers.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'; import 'package:uni/view/profile/profile.dart'; @@ -16,8 +15,13 @@ abstract class GeneralPageViewState extends State { final double borderMargin = 18.0; static ImageProvider? profileImageProvider; + Future onRefresh(BuildContext context); + + Future onLoad(BuildContext context) async {} + @override Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) => onLoad(context)); return getScaffold(context, getBody(context)); } @@ -27,9 +31,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( + null, Provider.of(context, listen: false).session, + forceRetrieval: forceRetrieval || profileImageProvider == null); return getProfileDecorationImage(profilePictureFile); } @@ -52,21 +57,14 @@ abstract class GeneralPageViewState extends State { Widget refreshState(BuildContext context, Widget child) { return RefreshIndicator( key: GlobalKey(), - onRefresh: refreshCallback(context), + onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture(null, + Provider.of(context, listen: false).session, + forceRetrieval: true) + .then((value) => onRefresh(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/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/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index f5f0b2d1d..c1e893c2f 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -1,90 +1,72 @@ 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/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; - static final AppLastUserInfoUpdateDatabase lastUpdateDatabase = - AppLastUserInfoUpdateDatabase(); @override Widget build(BuildContext context) { - return Consumer( - 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 && !hasContentPredicate) { + return loadingWidget(context); + } else if (status == RequestStatus.failed) { + return requestFailedMessage(); + } + + return hasContentPredicate + ? builder() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: onNullContent); + } + + Widget loadingWidget(BuildContext context) { + 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() { 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), @@ -92,9 +74,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_unit_info/course_unit_info.dart b/uni/lib/view/course_unit_info/course_unit_info.dart index b9a28c8cb..b3f40b3e0 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,16 @@ 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/lazy/course_units_info_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; +import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_classes.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_sheet.dart'; +import 'package:uni/view/lazy_consumer.dart'; class CourseUnitDetailPageView extends StatefulWidget { final CourseUnit courseUnit; @@ -16,24 +25,85 @@ class CourseUnitDetailPageView extends StatefulWidget { class CourseUnitDetailPageViewState extends SecondaryPageViewState { + Future loadInfo(bool force) async { + final courseUnitsProvider = + Provider.of(context, listen: false); + final session = context.read().session; + + final CourseUnitSheet? courseUnitSheet = + courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; + if (courseUnitSheet == null || force) { + courseUnitsProvider.fetchCourseUnitSheet(widget.courseUnit, session); + } + + final List? courseUnitClasses = + courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; + if (courseUnitClasses == null || force) { + courseUnitsProvider.fetchCourseUnitClasses(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 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: 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 LazyConsumer( + builder: (context, courseUnitsInfoProvider) { + 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: + 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..211843c1d --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.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/session_provider.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_student_row.dart'; + +class CourseUnitClassesView extends StatelessWidget { + final List classes; + + const CourseUnitClassesView(this.classes, {super.key}); + + @override + Widget build(BuildContext context) { + final Session session = context.read().session; + final List cards = []; + for (CourseUnitClass courseUnitClass in classes) { + final bool isMyClass = courseUnitClass.students + .where((student) => + student.number == + (int.tryParse(session.username.replaceAll(RegExp(r"\D"), "")) ?? + 0)) + .isNotEmpty; + cards.add(CourseUnitInfoCard( + isMyClass + ? '${courseUnitClass.className} *' + : courseUnitClass.className, + Column( + children: courseUnitClass.students + .map((student) => CourseUnitStudentRow(student, session)) + .toList(), + ))); + } + + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards)); + } +} diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart new file mode 100644 index 000000000..3295b04cd --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:uni/view/common_widgets/generic_expansion_card.dart'; + +class CourseUnitInfoCard extends GenericExpansionCard { + final String sectionTitle; + final Widget content; + + const CourseUnitInfoCard(this.sectionTitle, this.content, {key}) + : super( + key: key, + cardMargin: const EdgeInsets.only(bottom: 10), + smallTitle: true); + + @override + Widget buildCardContent(BuildContext context) { + return Container(padding: const EdgeInsets.only(top: 10), child: content); + } + + @override + String getTitle() { + return sectionTitle; + } +} 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..133100a7d --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -0,0 +1,107 @@ +import 'dart:math'; + +import 'package:flutter/material.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'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; + +class CourseUnitSheetView extends StatelessWidget { + final CourseUnitSheet courseUnitSheet; + + 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 = []; + for (var section in courseUnitSheet.sections.entries) { + cards.add(_buildCard(section.key, section.value, baseUrl)); + } + + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards)); + } + + CourseUnitInfoCard _buildCard( + String sectionTitle, String sectionContent, Uri baseUrl) { + return CourseUnitInfoCard( + 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_student_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart new file mode 100644 index 000000000..74b17ae3a --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart @@ -0,0 +1,57 @@ +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:uni/model/providers/startup/profile_provider.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 = + ProfileProvider.fetchOrGetCachedProfilePicture(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: 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 19a2b94e4..790a4e894 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -1,14 +1,15 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; +import 'package:uni/model/providers/startup/profile_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/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,11 +29,11 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, profileProvider, _) { - final List courseUnits = profileProvider.currUcs; + return LazyConsumer(builder: (context, profileProvider) { + final List courseUnits = profileProvider.profile.courseUnits; List availableYears = []; List availableSemesters = []; + if (courseUnits.isNotEmpty) { availableYears = _getAvailableYears(courseUnits); if (availableYears.isNotEmpty && selectedSchoolYear == null) { @@ -52,18 +53,16 @@ class CourseUnitsPageViewState ? availableSemesters[0] : availableSemesters[1]; } - - return _getPageView(courseUnits, profileProvider.status, availableYears, - availableSemesters); - } else { - return Container(); } + + return _getPageView(courseUnits, profileProvider.status, availableYears, + availableSemesters); }); } Widget _getPageView( List? courseUnits, - RequestStatus? requestStatus, + RequestStatus requestStatus, List availableYears, List availableSemesters) { final List? filteredCourseUnits = @@ -79,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, + status: requestStatus, + builder: () => + _generateCourseUnitsCards(filteredCourseUnits, context), + hasContentPredicate: courseUnits?.isNotEmpty ?? false, onNullContent: Center( heightFactor: 10, child: Text('Não existem cadeiras para apresentar', @@ -193,4 +191,10 @@ class CourseUnitsPageViewState .sorted() + [CourseUnitsPageView.bothSemestersDropdownOption]; } + + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } 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..dee0280c9 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'; @@ -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); @@ -42,4 +42,7 @@ class CourseUnitCard extends GenericCard { MaterialPageRoute( builder: (context) => CourseUnitDetailPageView(courseUnit))); } + + @override + void onRefresh(BuildContext context) {} } diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index b56bb0eb8..8ba02f47a 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -1,12 +1,14 @@ 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/lazy/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/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'; class ExamsPageView extends StatefulWidget { const ExamsPageView({super.key}); @@ -21,30 +23,38 @@ 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. 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; } @@ -108,4 +118,9 @@ class ExamsPageViewState extends GeneralPageViewState { } return Column(children: examCards); } + @override + Future onRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } \ No newline at end of file diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 24bfa5fc9..b9f08e47f 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -1,9 +1,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; @@ -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_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 578da2b37..a7e94d3ec 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -1,13 +1,11 @@ -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; @@ -79,12 +77,10 @@ class _ExamRowState extends State { onPressed: () => setState(() { Provider.of(context, listen: false) - .toggleHiddenExam( - widget.exam.id, Completer()); + .toggleHiddenExam(widget.exam.id); })), IconButton( - icon: const Icon(MdiIcons.calendarPlus, - size: 30), + icon: Icon(MdiIcons.calendarPlus, size: 30), onPressed: () => Add2Calendar.addEvent2Cal( createExamEvent())), ]), 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/home/home.dart b/uni/lib/view/home/home.dart index 613e0c18a..579e463ab 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/providers/lazy/home_page_provider.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; @@ -13,6 +15,19 @@ class HomePageView extends StatefulWidget { class HomePageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return MainCardsList(); + return const MainCardsList(); + } + + @override + Future onRefresh(BuildContext context) async { + final favoriteCardTypes = context.read().favoriteCards; + final cards = favoriteCardTypes + .map((e) => + MainCardsList.cardCreators[e]!(const Key(""), false, () => null)) + .toList(); + + for (final card in cards) { + card.onRefresh(context); + } } } diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index ff567736a..0e66d9cae 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -1,13 +1,14 @@ 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/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'; 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 +25,23 @@ class BusStopCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( - builder: (context, busProvider, _) { - return getCardContent(context, busProvider.configuredBusStops, busProvider.status); + return LazyConsumer( + builder: (context, busProvider) { + return getCardContent( + context, busProvider.configuredBusStops, busProvider.status); }, ); } + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } } /// 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) { @@ -113,7 +121,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) { diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index 331a48e26..b7b7f4fe9 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -1,15 +1,16 @@ 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/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 { @@ -26,24 +27,27 @@ 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 /// 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 .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/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 857a55140..72656370f 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -1,25 +1,25 @@ 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/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'; -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); 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,41 +37,40 @@ class MainCardsList extends StatelessWidget { LibraryOccupationCard.fromEditingInformation(k, em, od) }; - MainCardsList({super.key}); + const MainCardsList({super.key}); @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, )); @@ -104,13 +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; + 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(), @@ -136,16 +135,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 +152,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 +160,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 +177,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 +193,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 4eeb035d6..8efb549e8 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.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'; 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); @@ -18,22 +19,26 @@ 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) { - return Consumer( - 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)))); + return LazyConsumer( + builder: (context, restaurantProvider) => RequestDependentWidgetBuilder( + status: restaurantProvider.status, + 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, + 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 a17fdc2d0..a525b8e96 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -2,13 +2,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/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/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); @@ -22,21 +23,22 @@ class ScheduleCard extends GenericCard { final List lectures = []; @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)) - ); + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } + @override + Widget buildCardContent(BuildContext context) { + return LazyConsumer( + builder: (context, lectureProvider) => RequestDependentWidgetBuilder( + status: lectureProvider.status, + 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, + textAlign: TextAlign.center)), + contentLoadingWidget: const ScheduleCardShimmer().build(context))); } Widget generateSchedule(lectures, BuildContext context) { @@ -58,7 +60,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 +72,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..6a83b135f --- /dev/null +++ b/uni/lib/view/lazy_consumer.dart @@ -0,0 +1,55 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/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. +/// 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; + + const LazyConsumer({ + Key? key, + required this.builder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + 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) { + 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 + } + }); + + 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 0f4f1d7a1..6503b84a6 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -2,9 +2,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/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'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; class LibraryPageView extends StatefulWidget { @@ -17,24 +18,15 @@ class LibraryPageView extends StatefulWidget { class LibraryPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, libraryOccupationProvider, _) => + return LazyConsumer( + 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); - } - }); - */ + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); } } diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index bcaa96d43..d4265d2c5 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -1,11 +1,12 @@ 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/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'; 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 { @@ -22,16 +23,21 @@ 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 Consumer( - builder: (context, libraryOccupationProvider, _) => + 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/locations/locations.dart b/uni/lib/view/locations/locations.dart index b9ce8722c..e2905c0c0 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -1,14 +1,12 @@ 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/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'; -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'; +import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationsPage extends StatefulWidget { const LocationsPage({Key? key}) : super(key: key); @@ -28,59 +26,50 @@ class LocationsPageState extends GeneralPageViewState @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, locationsProvider, _) { + return LazyConsumer( + builder: (context, locationsProvider) { return LocationsPageView( locations: locationsProvider.locations, status: locationsProvider.status); }, ); } + + @override + Future onRefresh(BuildContext context) async {} } 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, required this.status}); @override Widget build(BuildContext context) { return Column(mainAxisSize: MainAxisSize.max, children: [ - upperMenuContainer(context), Container( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - height: MediaQuery.of(context).size.height * 0.75, - alignment: Alignment.center, - child: //TODO:: add support for multiple faculties - getMap(context), - ) + width: MediaQuery.of(context).size.width * 0.95, + padding: const EdgeInsets.fromLTRB(0, 0, 0, 4.0), + child: PageTitle(name: 'Locais: ${getLocation()}')), + Container( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: MediaQuery.of(context).size.height * 0.75, + alignment: Alignment.center, + child: RequestDependentWidgetBuilder( + status: status, + builder: () => FacultyMap(faculty: "FEUP", locations: locations), + hasContentPredicate: locations.isNotEmpty, + onNullContent: + const Center(child: Text('Não existem locais disponíveis')), + ) + // TODO: add support for multiple faculties + ) ]); } - Container upperMenuContainer(BuildContext context) { - return Container( - width: MediaQuery.of(context).size.width * 0.95, - padding: const EdgeInsets.fromLTRB(0, 0, 0, 4.0), - child: PageTitle(name: 'Locais: ${getLocation()}')); - //TODO:: add support for multiple faculties - } - - LocationsMap? getMap(BuildContext context) { - if (locations == null || status != RequestStatus.successful) { - return null; - } - return FacultyMaps.getFeupMap(locations!); - } - String getLocation() { return 'FEUP'; } - - List getMarkers() { - return locations!.map((location) { - return LocationMarker(location.latlng, location); - }).toList(); - } } diff --git a/uni/lib/view/locations/widgets/faculty_map.dart b/uni/lib/view/locations/widgets/faculty_map.dart new file mode 100644 index 000000000..b0398de0b --- /dev/null +++ b/uni/lib/view/locations/widgets/faculty_map.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +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; + + const FacultyMap({Key? key, required this.faculty, required this.locations}) + : super(key: key); + + @override + Widget build(BuildContext context) { + switch (faculty) { + case 'FEUP': + return LocationsMap( + northEastBoundary: LatLng(41.17986, -8.59298), + southWestBoundary: LatLng(41.17670, -8.59991), + center: LatLng(41.17731, -8.59522), + locations: locations, + ); + default: + return Container(); // Should not happen + } + } + + static Color 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/faculty_maps.dart b/uni/lib/view/locations/widgets/faculty_maps.dart deleted file mode 100644 index 7d113e654..000000000 --- a/uni/lib/view/locations/widgets/faculty_maps.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:uni/model/entities/location_group.dart'; -import 'package:uni/view/locations/widgets/map.dart'; - -class FacultyMaps { - static LocationsMap? getFacultyMap( - String faculty, List locations) { - switch (faculty) { - case 'FEUP': - return getFeupMap(locations); - } - return null; - } - - static LocationsMap getFeupMap(List locations) { - return LocationsMap( - northEastBoundary: LatLng(41.17986, -8.59298), - southWestBoundary: LatLng(41.17670, -8.59991), - center: LatLng(41.17731, -8.59522), - locations: locations, - ); - } - - 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..cc156f16a 100644 --- a/uni/lib/view/locations/widgets/floorless_marker_popup.dart +++ b/uni/lib/view/locations/widgets/floorless_marker_popup.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; import 'package:uni/model/entities/location_group.dart'; -import 'package:uni/view/locations/widgets/faculty_maps.dart'; +import 'package:uni/view/locations/widgets/faculty_map.dart'; class FloorlessLocationMarkerPopup extends StatelessWidget { const FloorlessLocationMarkerPopup(this.locationGroup, @@ -27,7 +27,9 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { children: (showId ? [Text(locationGroup.id.toString())] : []) + - buildLocations(context, locations), + locations + .map((location) => LocationRow(location: location)) + .toList(), )), ); } @@ -39,9 +41,27 @@ class FloorlessLocationMarkerPopup extends StatelessWidget { children: [ Text(location.description(), textAlign: TextAlign.left, - style: TextStyle(color: FacultyMaps.getFontColor(context))) + style: TextStyle(color: FacultyMap.getFontColor(context))) ], )) .toList(); } } + +class LocationRow extends StatelessWidget { + final Location location; + + const LocationRow({Key? key, required this.location}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(location.description(), + textAlign: TextAlign.left, + style: TextStyle(color: FacultyMap.getFontColor(context))) + ], + ); + } +} diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index 6eb951323..74df51ed3 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -64,39 +64,37 @@ class LocationsMap extends StatelessWidget { subdomains: const ['a', 'b', 'c'], tileProvider: CachedTileProvider(), ), - PopupMarkerLayerWidget( + PopupMarkerLayer( options: PopupMarkerLayerOptions( - markers: _getMarkers(), + 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')); + }, + ), ), ), ]); } - - List _getMarkers() { - return locations.map((location) { - return LocationMarker(location.latlng, location); - }).toList(); - } } class CachedTileProvider extends TileProvider { CachedTileProvider(); @override - ImageProvider getImage(Coords coords, TileLayer options) { + ImageProvider getImage(TileCoordinates coordinates, TileLayer options) { return CachedNetworkImageProvider( - getTileUrl(coords, options), + getTileUrl(coordinates, options), ); } } diff --git a/uni/lib/view/locations/widgets/marker.dart b/uni/lib/view/locations/widgets/marker.dart index d3cca2d33..626f57dff 100644 --- a/uni/lib/view/locations/widgets/marker.dart +++ b/uni/lib/view/locations/widgets/marker.dart @@ -3,7 +3,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:uni/model/entities/location.dart'; import 'package:uni/model/entities/location_group.dart'; -import 'package:uni/view/locations/widgets/faculty_maps.dart'; +import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationMarker extends Marker { final LocationGroup locationGroup; @@ -22,18 +22,26 @@ class LocationMarker extends Marker { color: Theme.of(ctx).colorScheme.primary, ), borderRadius: const BorderRadius.all(Radius.circular(20))), - child: getIcon(locationGroup.getLocationWithMostWeight(), ctx), + child: + MarkerIcon(location: locationGroup.getLocationWithMostWeight()), ), ); +} + +class MarkerIcon extends StatelessWidget { + final Location? location; + + const MarkerIcon({Key? key, this.location}) : super(key: key); - static Widget getIcon(Location? location, BuildContext context) { + @override + Widget build(BuildContext context) { if (location == null) { return Container(); } - final Color fontColor = FacultyMaps.getFontColor(context); - if (location.icon is IconData) { - return Icon(location.icon, color: fontColor, size: 12); + final Color fontColor = FacultyMap.getFontColor(context); + if (location?.icon is IconData) { + return Icon(location?.icon, color: fontColor, size: 12); } else { return Icon(Icons.device_unknown, color: fontColor, size: 12); } diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 87b653fd8..7802b1f97 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; import 'package:uni/model/entities/location_group.dart'; -import 'package:uni/view/locations/widgets/faculty_maps.dart'; +import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationMarkerPopup extends StatelessWidget { const LocationMarkerPopup(this.locationGroup, @@ -25,28 +25,32 @@ class LocationMarkerPopup extends StatelessWidget { children: (showId ? [Text(locationGroup.id.toString())] : []) + - buildFloors(context), + getEntries() + .map((entry) => + Floor(floor: entry.key, locations: entry.value)) + .toList(), )), ); } - List buildFloors(BuildContext context) { - //Sort by floor + List>> getEntries() { final List>> entries = locationGroup.floors.entries.toList(); entries.sort((current, next) => -current.key.compareTo(next.key)); + return entries; + } +} - return entries.map((entry) { - final int floor = entry.key; - final List locations = entry.value; +class Floor extends StatelessWidget { + final List locations; + final int floor; - return Row(children: buildFloor(context, floor, locations)); - }).toList(); - } + const Floor({Key? key, required this.locations, required this.floor}) + : super(key: key); - List buildFloor( - BuildContext context, floor, List locations) { - final Color fontColor = FacultyMaps.getFontColor(context); + @override + Widget build(BuildContext context) { + final Color fontColor = FacultyMap.getFontColor(context); final String floorString = 0 <= floor && floor <= 9 //To maintain layout of popup @@ -69,21 +73,30 @@ class LocationMarkerPopup extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: buildLocations(context, locations, fontColor), + children: locations + .map((location) => + LocationRow(location: location, color: fontColor)) + .toList(), )); - return [floorCol, locationsColumn]; + return Row(children: [floorCol, locationsColumn]); } +} + +class LocationRow extends StatelessWidget { + final Location location; + final 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)) - ], - )) - .toList(); + const LocationRow({Key? key, required this.location, required this.color}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(location.description(), + textAlign: TextAlign.left, style: TextStyle(color: color)) + ], + ); } } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 571e6b2a4..000ce1926 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -3,14 +3,15 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/request_status.dart'; -import 'package:uni/model/providers/session_provider.dart'; +import 'package:uni/model/entities/login_exceptions.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; +import 'package:uni/model/request_status.dart'; +import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; import 'package:uni/view/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'; class LoginPageView extends StatefulWidget { const LoginPageView({super.key}); @@ -44,18 +45,31 @@ 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, stateProviders, - _keepSignedIn, usernameController, passwordController); - completer.future - .whenComplete(() => handleLogin(sessionProvider.status, context)); + + try { + await sessionProvider.postAuthentication( + 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, 'Erro no login'); + } + } } } @@ -105,35 +119,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; @@ -225,13 +240,53 @@ 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}'); - } else if (status == RequestStatus.failed) { - ToastMessage.error(context, 'O login falhou'); } } + + void updatePasswordDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("A tua palavra-passe expirou"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 20), + const Align( + alignment: Alignment.centerLeft, + child: Text( + 'Deseja alterar a palavra-passe?', + textAlign: TextAlign.start, + )), + ], + ), + actions: [ + TextButton( + child: const Text("Cancelar"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + child: const Text("Alterar"), + onPressed: () async { + const url = "https://self-id.up.pt/password"; + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } + }, + ), + ], + ); + }, + ); + } } 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); } } diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index e1165559a..864ad4459 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,8 +1,9 @@ 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/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'; import 'package:uni/view/profile/widgets/course_info_card.dart'; import 'package:uni/view/profile/widgets/profile_overview.dart'; @@ -18,28 +19,27 @@ class ProfilePageView extends StatefulWidget { class ProfilePageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, profileStateProvider, _) { + 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(), + ]); }, ); } @@ -48,4 +48,10 @@ class ProfilePageViewState extends SecondaryPageViewState { Widget getTopRightButton(BuildContext context) { return Container(); } + + @override + Future onRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 47a6363f5..5e262ec20 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/profile_state_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'; import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/profile/widgets/reference_section.dart'; import 'package:uni/view/profile/widgets/tuition_notification_switch.dart'; /// Manages the 'Current account' section inside the user's page (accessible @@ -13,11 +18,22 @@ 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 Consumer( - builder: (context, profileStateProvider, _) { + return LazyConsumer( + builder: (context, profileStateProvider) { + return LazyConsumer( + builder: (context, referenceProvider) { final profile = profileStateProvider.profile; + final List references = referenceProvider.references; + return Column(children: [ Table( columnWidths: const {1: FractionColumnWidth(.4)}, @@ -45,27 +61,38 @@ 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)), + ])), + ReferenceList(references: references), + const SizedBox(height: 10), showLastRefreshedTime(profileStateProvider.feesRefreshTime, context) ]); - }, - ); + }); + }); } @override @@ -74,3 +101,35 @@ class AccountInfoCard extends GenericCard { @override onClick(BuildContext context) {} } + +class ReferenceList extends StatelessWidget { + final List references; + + const ReferenceList({Key? key, required this.references}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (references.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + "Não existem referências a pagar", + style: Theme.of(context).textTheme.titleSmall, + textScaleFactor: 0.96, + ), + ); + } + 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]), + ]); + } +} diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index 86fb0b052..8b7eeeb27 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -103,4 +103,7 @@ class CourseInfoCard extends GenericCard { @override onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) {} } 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 eb0155295..d53c38bbb 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/providers/profile_state_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'; class PrintInfoCard extends GenericCard { @@ -13,8 +14,8 @@ class PrintInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return Consumer( - builder: (context, profileStateProvider, _) { + return LazyConsumer( + builder: (context, profileStateProvider) { final profile = profileStateProvider.profile; return Column( mainAxisSize: MainAxisSize.min, @@ -47,8 +48,7 @@ class PrintInfoCard extends GenericCard { ), onPressed: () => addMoneyDialog(context), child: const Center(child: Icon(Icons.add)), - ) - ), + )), ]) ]), showLastRefreshedTime( @@ -64,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/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index 99f142d0b..d313e3a38 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -2,51 +2,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/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.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( builder: (context, sessionProvider, _) { return FutureBuilder( - future: loadProfilePicture(sessionProvider.session), + future: ProfileProvider.fetchOrGetCachedProfilePicture( + null, 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/profile/widgets/reference_section.dart b/uni/lib/view/profile/widgets/reference_section.dart new file mode 100644 index 000000000..d32628460 --- /dev/null +++ b/uni/lib/view/profile/widgets/reference_section.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +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; + + const ReferenceSection({Key? key, required this.reference}) : super(key: key); + + @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), + ]); + } +} + +class InfoText extends StatelessWidget { + final String text; + final Color? color; + + 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), + ); + } +} + +class TitleText extends StatelessWidget { + final String title; + + const TitleText({Key? key, required this.title}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 20.0), + alignment: Alignment.centerLeft, + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall, + overflow: TextOverflow.fade, + softWrap: false, + ), + ); + } +} + +class InfoCopyRow extends StatelessWidget { + final String infoName; + final String info; + 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); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 20.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InfoText(text: infoName), + const Spacer(), + InfoText(text: "${isMoney ? _getMoneyAmount() : info} "), + InkWell( + splashColor: Theme.of(context).highlightColor, + child: const Icon(Icons.content_copy, size: 16), + onTap: () { + Clipboard.setData(ClipboardData(text: info)); + ToastMessage.success(context, copyMessage); + }, + ), + ], + ), + ); + } + + String _getMoneyAmount() => + NumberFormat.simpleCurrency(locale: 'eu').format(double.parse(info)); +} diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 8d5144c51..2ff5bd339 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -1,14 +1,13 @@ +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/meal.dart'; -import 'package:flutter/material.dart'; -import 'package:uni/model/providers/restaurant_provider.dart'; -import 'package:uni/model/request_status.dart'; +import 'package:uni/model/entities/restaurant.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'; -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'; @@ -16,10 +15,10 @@ class RestaurantPageView extends StatefulWidget { const RestaurantPageView({Key? key}) : super(key: key); @override - State createState() => _CanteenPageState(); + State createState() => _RestaurantPageState(); } -class _CanteenPageState extends GeneralPageViewState +class _RestaurantPageState extends GeneralPageViewState with SingleTickerProviderStateMixin { late List aggRestaurant; late TabController tabController; @@ -31,60 +30,56 @@ 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))); + tabController.animateTo((tabController.index + (weekDay - 1))); scrollViewController = ScrollController(); } @override Widget getBody(BuildContext context) { - return Consumer( - builder: (context, restaurantProvider, _) => - _getPageView(restaurantProvider.restaurants, restaurantProvider.status)); - - } - - Widget _getPageView(List restaurants, RequestStatus? status) { - 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: status ?? RequestStatus.none, - contentGenerator: createTabViewBuilder, - content: restaurants, - contentChecker: restaurants.isNotEmpty, - onNullContent: - const Center(child: Text('Não há refeições disponíveis.'))) - ]); + 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( + status: restaurantProvider.status, + builder: () => + createTabViewBuilder(restaurantProvider.restaurants, context), + hasContentPredicate: 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 cantinesWidgets = []; - if (restaurants is List) { - cantinesWidgets = restaurants - .map((restaurant) => createRestaurant(context, restaurant, dayOfWeek)) - .toList(); - } - return ListView( children: cantinesWidgets,); - }).toList(); - - return Expanded( - child: TabBarView( - controller: tabController, - children: dayContents, - )); + 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( + child: TabBarView( + controller: tabController, + children: dayContents, + )); } List createTabs(BuildContext context) { @@ -93,34 +88,40 @@ class _CanteenPageState 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])), )); } return tabs; } - Widget createRestaurant(context, Restaurant restaurant, DayOfWeek dayOfWeek) { - return RestaurantPageCard( - restaurant.name, createRestaurantByDay(context, restaurant, dayOfWeek)); + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); } +} - List createRestaurantRows(List meals, BuildContext context) { - return meals - .map((meal) => RestaurantSlot(type: meal.type, name: meal.name)) - .toList(); - } +class RestaurantDay extends StatelessWidget { + final Restaurant restaurant; + final DayOfWeek day; - Widget createRestaurantByDay( - BuildContext context, Restaurant restaurant, DayOfWeek day) { + const RestaurantDay({Key? key, required this.restaurant, required this.day}) + : super(key: key); + + @override + Widget build(BuildContext context) { final List meals = restaurant.getMealsOfDay(day); if (meals.isEmpty) { return Container( margin: const EdgeInsets.only(top: 10, bottom: 5), - key: Key('cantine-page-day-column-$day'), + key: Key('restaurant-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, children: const [ + SizedBox(height: 10), Center( child: Text("Não há informação disponível sobre refeições")), ], @@ -128,10 +129,12 @@ class _CanteenPageState extends GeneralPageViewState } else { return Container( margin: const EdgeInsets.only(top: 5, bottom: 5), - key: Key('cantine-page-day-column-$day'), + key: Key('restaurant-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, - children: createRestaurantRows(meals, context), + children: meals + .map((meal) => RestaurantSlot(type: meal.type, name: meal.name)) + .toList(), )); } } diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index 062fe8a88..ca2fae3e4 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) { @@ -21,4 +21,7 @@ class RestaurantPageCard extends GenericCard { @override onClick(BuildContext context) {} + + @override + void onRefresh(BuildContext context) {} } diff --git a/uni/lib/view/restaurant/widgets/restaurant_slot.dart b/uni/lib/view/restaurant/widgets/restaurant_slot.dart index 7f30d6de3..564e62ae2 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_slot.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_slot.dart @@ -11,15 +11,6 @@ class RestaurantSlot extends StatelessWidget { required this.name, }) : super(key: key); - static const mealTypeIcons = { - 'sopa': 'assets/meal-icons/soup.svg', - 'carne': 'assets/meal-icons/chicken.svg', - 'peixe': 'assets/meal-icons/fish.svg', - 'dieta': 'assets/meal-icons/diet.svg', - 'vegetariano': 'assets/meal-icons/vegetarian.svg', - 'salada': 'assets/meal-icons/salad.svg', - }; - @override Widget build(BuildContext context) { return Container( @@ -33,7 +24,7 @@ class RestaurantSlot extends StatelessWidget { margin: const EdgeInsets.fromLTRB(0, 0, 8.0, 0), child: SizedBox( width: 20, - child: createCanteenSlotType(context), + child: RestaurantSlotType(type: type), )), Flexible( child: Text( @@ -45,24 +36,39 @@ class RestaurantSlot extends StatelessWidget { )), ); } +} - Widget createCanteenSlotType(context) { - final mealsType = type.toLowerCase(); +class RestaurantSlotType extends StatelessWidget { + final String type; - final icon = mealTypeIcons.entries - .firstWhere((element) => mealsType.contains(element.key), - orElse: () => const MapEntry('', '')) - .value; + static const mealTypeIcons = { + 'sopa': 'assets/meal-icons/soup.svg', + 'carne': 'assets/meal-icons/chicken.svg', + 'peixe': 'assets/meal-icons/fish.svg', + 'dieta': 'assets/meal-icons/diet.svg', + 'vegetariano': 'assets/meal-icons/vegetarian.svg', + 'salada': 'assets/meal-icons/salad.svg', + }; + + const RestaurantSlotType({Key? key, required this.type}) : super(key: key); + @override + Widget build(BuildContext context) { + final String icon = getIcon(); return Tooltip( message: type, child: icon != '' ? SvgPicture.asset( icon, colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), + Theme.of(context).primaryColor, BlendMode.srcIn), height: 20, ) : null); } + + String getIcon() => mealTypeIcons.entries + .firstWhere((element) => type.toLowerCase().contains(element.key), + orElse: () => const MapEntry('', '')) + .value; } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 8d9ac565b..bb6800c51 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -1,13 +1,15 @@ 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/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'; 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 { @@ -20,8 +22,8 @@ class SchedulePage extends StatefulWidget { class SchedulePageState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, lectureProvider, _) { + return LazyConsumer( + builder: (context, lectureProvider) { return SchedulePageView( lectures: lectureProvider.lectures, scheduleStatus: lectureProvider.status, @@ -51,8 +53,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); } @@ -155,32 +156,33 @@ 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, - onNullContent: Center( - child: Text( - 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.')), - ); + 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), + ))); + } + + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); } } diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index de4266557..c6571076b 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -32,47 +32,82 @@ 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-${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, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: createScheduleSlotPrimInfo(context), + )), )); } - Widget createScheduleSlotRow(context) { - return Container( - 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, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: createScheduleSlotPrimInfo(context), - )); + List createScheduleSlotPrimInfo(context) { + final subjectTextField = TextFieldWidget( + text: subject, + style: Theme.of(context) + .textTheme + .headlineSmall! + .apply(color: Theme.of(context).colorScheme.tertiary), + alignment: TextAlign.center); + final typeClassTextField = TextFieldWidget( + text: ' ($typeClass)', + style: Theme.of(context).textTheme.bodyMedium, + alignment: TextAlign.center); + final roomTextField = TextFieldWidget( + text: rooms, + style: Theme.of(context).textTheme.bodyMedium, + alignment: TextAlign.right); + return [ + ScheduleTimeWidget( + begin: DateFormat("HH:mm").format(begin), + end: DateFormat("HH:mm").format(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 createScheduleSlotTime(context) { - return Column( - 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) - ], - ); - } +class SubjectButtonWidget extends StatelessWidget { + final int occurrId; - Widget createScheduleTime(String time, context) => createTextField( - time, Theme.of(context).textTheme.bodyMedium, TextAlign.center); + 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: [ @@ -90,55 +125,81 @@ class ScheduleSlot extends StatelessWidget { ], ); } +} - List createScheduleSlotPrimInfo(context) { - final subjectTextField = createTextField( - subject, - Theme.of(context) - .textTheme - .headlineSmall! - .apply(color: Theme.of(context).colorScheme.tertiary), - TextAlign.center); - final typeClassTextField = createTextField(' ($typeClass)', - Theme.of(context).textTheme.bodyMedium, TextAlign.center); - final roomTextField = createTextField( - rooms, Theme.of(context).textTheme.bodyMedium, 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.bodyMedium, + 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), + ], + ); } +} + +class ScheduleTimeTextField extends StatelessWidget { + final String time; + final BuildContext context; - Widget createScheduleSlotTeacherClassInfo(context) { - return createTextField( - classNumber != null ? '$classNumber | $teacher' : teacher, - Theme.of(context).textTheme.bodyMedium, - TextAlign.center); + const ScheduleTimeTextField( + {super.key, required this.time, required this.context}); + + @override + Widget build(BuildContext context) { + return TextFieldWidget( + text: time, + style: Theme.of(context).textTheme.bodyMedium, + 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, + ); } } diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index caa578bc4..2496143a4 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,39 +101,39 @@ 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 = await AppSharedPreferences.getUserFaculties(); stateProviders.sessionProvider - .reLogin(userName, password, faculties, stateProviders); + .restoreSession(userName, password, faculties); } return MaterialPageRoute(builder: (context) => const HomePageView()); 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!; - } } 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, ), )); diff --git a/uni/lib/view/useful_info/useful_info.dart b/uni/lib/view/useful_info/useful_info.dart index cb6b83dc7..c575a105c 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 onRefresh(BuildContext context) async {} } diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index a559742ed..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.20+138 +version: 1.5.43+161 environment: sdk: ">=2.17.1 <3.0.0" @@ -43,42 +43,42 @@ dependencies: encrypt: ^5.0.0-beta.1 path_provider: ^2.0.0 sqflite: ^2.0.3 - path: ^1.8.0 - cached_network_image: ^3.0.0-nullsafety + path: ^1.8.0 flutter_svg: ^2.0.0+1 synchronized: ^3.0.0 image: ^4.0.13 - connectivity_plus: ^3.0.3 + connectivity_plus: ^4.0.1 logger: ^1.1.0 url_launcher: ^6.0.2 flutter_markdown: ^0.6.0 - intl: ^0.17.0 + intl: ^0.18.1 crypto: ^3.0.1 add_2_calendar: ^2.1.3 - sentry_flutter: ^6.5.1 + sentry_flutter: ^7.5.2 email_validator: ^2.0.1 currency_text_input_formatter: ^2.1.5 - expansion_tile_card: ^2.0.0 + expansion_tile_card: ^3.0.0 collection: ^1.16.0 timelines: ^0.1.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. + flutter_map: ^4.0.0 + 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: ^12.0.4 + flutter_local_notifications: ^15.1.0+1 percent_indicator: ^4.2.2 - shimmer: ^2.0.0 - material_design_icons_flutter: ^6.0.7096 + flutter_widget_from_html_core: ^0.10.3 + shimmer: ^3.0.0 + material_design_icons_flutter: ^7.0.7296 + flutter_dotenv: ^5.0.2 dev_dependencies: flutter_test: sdk: flutter test: any mockito: ^5.2.0 - flutter_launcher_icons: ^0.12.0 + flutter_launcher_icons: ^0.13.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index f8adeed87..b975df0f5 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -1,7 +1,7 @@ // @dart=2.10 -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; @@ -11,11 +11,11 @@ import 'package:tuple/tuple.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/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'; @@ -29,25 +29,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')]; @@ -74,17 +82,13 @@ void main() { expect(find.byKey(Key('$sopeExam-exam')), findsNothing); expect(find.byKey(Key('$mdisExam-exam')), findsNothing); - final Completer completer = Completer(); - examProvider.getUserExams( - completer, + await examProvider.fetchUserExams( ParserExams(), const Tuple2('', ''), profile, - Session(authenticated: true), + Session(username: '', cookies: '', faculties: ['feup']), [sopeCourseUnit, sdisCourseUnit]); - await completer.future; - await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); @@ -113,27 +117,21 @@ void main() { expect(find.byKey(Key('$sdisExam-exam')), findsNothing); expect(find.byKey(Key('$sopeExam-exam')), findsNothing); - final Completer completer = Completer(); - examProvider.getUserExams( - completer, + await examProvider.fetchUserExams( ParserExams(), const Tuple2('', ''), profile, - Session(authenticated: true), + Session(username: '', cookies: '', faculties: ['feup']), [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(); @@ -160,7 +158,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..2ac6230c5 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -1,8 +1,8 @@ // @dart=2.10 -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; @@ -13,8 +13,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/last_user_info_provider.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'; @@ -32,91 +31,88 @@ 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); + + await scheduleProvider.fetchUserLectures(const Tuple2('', ''), + Session(username: '', cookies: '', faculties: ['feup']), profile); + + 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..4aacd7a8d 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -1,17 +1,15 @@ // @dart=2.10 -import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/networking/network_router.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/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'; @@ -23,25 +21,31 @@ 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('', ''); final profile = Profile(); profile.courses = [Course(id: 7474)]; - final session = Session(authenticated: true); + final session = Session(username: '', cookies: '', faculties: ['feup']); final userUcs = [sopeCourseUnit, sdisCourseUnit]; NetworkRouter.httpClient = mockClient; @@ -53,20 +57,15 @@ void main() { setUp(() { provider = ExamProvider(); - expect(provider.status, RequestStatus.none); + expect(provider.status, RequestStatus.busy); }); test('When given one exam', () async { - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {sopeExam}); - - final action = Completer(); - - provider.getUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - - expect(provider.status, RequestStatus.busy); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {sopeExam}); - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.exams.isNotEmpty, true); expect(provider.exams, [sopeExam]); @@ -77,14 +76,8 @@ void main() { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam}); - final Completer action = Completer(); - - provider.getUserExams( - 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]); @@ -94,40 +87,31 @@ 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'); - - final Completer action = Completer(); + 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', + 'feup'); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam, specialExam}); - provider.getUserExams( - 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.getUserExams( - 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); }); @@ -135,18 +119,14 @@ 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}); - - final Completer action = Completer(); - - provider.getUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); @@ -155,18 +135,14 @@ 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}); - - final Completer action = Completer(); - - provider.getUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, []); @@ -175,18 +151,14 @@ 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'); - - when(parserExams.parseExams(any, any)).thenAnswer((_) async => {todayExam}); - - final Completer action = Completer(); + final todayExam = Exam('1234', before, after, 'SDIS', rooms, + 'Recurso - Época Recurso (1ºS)', 'feup'); - provider.getUserExams( - action, parserExams, userPersistentInfo, profile, session, userUcs); - expect(provider.status, RequestStatus.busy); + when(parserExams.parseExams(any, any)) + .thenAnswer((_) async => {todayExam}); - await action.future; + await provider.fetchUserExams( + parserExams, userPersistentInfo, profile, session, userUcs); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 72611fec1..e97e66be6 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -1,6 +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'; @@ -9,8 +8,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'; @@ -23,7 +21,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: ['feup']); final day = DateTime(2021, 06, 01); final lecture1 = Lecture.fromHtml( @@ -39,35 +37,25 @@ void main() { LectureProvider provider; setUp(() { provider = LectureProvider(); - expect(provider.status, RequestStatus.none); + expect(provider.status, RequestStatus.busy); }); test('When given a single schedule', () async { - final Completer action = Completer(); - when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => [lecture1, lecture2]); - provider.getUserLectures(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.getUserLectures(action, userPersistentInfo, session, profile); - expect(provider.status, RequestStatus.busy); - - await action.future; + await provider.fetchUserLectures(userPersistentInfo, session, profile); expect(provider.status, RequestStatus.failed); }); 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/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)); 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 = [