diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index db82b56ed..a428d898c 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,8 +1,11 @@ version: 2 -enable-beta-ecosystems: true updates: - package-ecosystem: "pub" directory: "/uni" schedule: - interval: "weekly" \ No newline at end of file + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + diff --git a/pre-commit-hook.sh b/pre-commit-hook.sh index c8f93c62b..56ec951b7 100755 --- a/pre-commit-hook.sh +++ b/pre-commit-hook.sh @@ -3,7 +3,7 @@ mkdir -p .git/hooks #it seems that are some cases where git will not create a ho tee .git/hooks/pre-commit << EOF #!/bin/sh -FILES="\$(git diff --name-only --cached | grep .*\.dart | grep -v .*\.g\.dart | grep -v .*\.mocks\.dart)" +FILES="\$(git diff --diff-filter=d --name-only --cached | grep .*\.dart | grep -v .*\.g\.dart | grep -v .*\.mocks\.dart)" [ -z "\$FILES" ] && exit 0 diff --git a/uni/.gitignore b/uni/.gitignore index ef53d37aa..39315f3b5 100644 --- a/uni/.gitignore +++ b/uni/.gitignore @@ -125,3 +125,6 @@ app.*.symbols !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock + +# Flutter Devtools +devtools_options.yaml diff --git a/uni/.metadata b/uni/.metadata new file mode 100644 index 000000000..b7a08be36 --- /dev/null +++ b/uni/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/uni/app_version.txt b/uni/app_version.txt index 54fadd55b..1e25189a6 100644 --- a/uni/app_version.txt +++ b/uni/app_version.txt @@ -1 +1 @@ -1.7.22+235 +1.8.0-beta.19+252 diff --git a/uni/assets/env/.env.example b/uni/assets/env/.env.example new file mode 100644 index 000000000..f0f399f01 --- /dev/null +++ b/uni/assets/env/.env.example @@ -0,0 +1,3 @@ +# Plausbile +PLAUSIBLE_URL=https://plausible.example.com +PLAUSIBLE_DOMAIN=your.plausible.domain diff --git a/uni/assets/images/no_wifi.png b/uni/assets/images/no_wifi.png new file mode 100644 index 000000000..b407d9480 Binary files /dev/null and b/uni/assets/images/no_wifi.png differ diff --git a/uni/assets/text/TermsAndConditions.md b/uni/assets/text/TermsAndConditions.md index 9d4beaf66..ed9294150 100644 --- a/uni/assets/text/TermsAndConditions.md +++ b/uni/assets/text/TermsAndConditions.md @@ -1,53 +1,143 @@ # App desenvolvida pelo NIAEFEUP. + # De estudantes, para estudantes. -\ -\ -**Terms and conditions** -These terms and conditions ("Terms", "Agreement") are an agreement between NIAEFEUP ("us", "we" or "our"), Núcleo de Informática da Associação de Estudantes da Universidade do Porto, and you ("User", "you" or "your"). This Agreement sets forth the general terms and conditions of your use of the uni mobile application and any of its products or services (collectively, "Mobile Application" or "Services"). \ \ -**Accounts and membership** +**Terms and Conditions** -To use the Mobile Application, you are required to hold a digital account provided by Universidade do Porto. We are in no way affiliated with this university. Any act performed by you through the Mobile Application is subject to the [Universidade do Porto's terms and conditions](https://sigarra.up.pt/up/pt/web_base.gera_pagina?p_pagina=termos%20e%20condicoes). -By using your faculty account in the Mobile Application, you are responsible for maintaining the security of your account and you are fully responsible for all activities that occur under the account and any other actions taken in connection with it. We will not be liable for any acts or omissions by you, including any damages of any kind incurred as a result of such acts or omissions. -Any interaction with your data from Universidade do Porto's platform is made directly through it, and is not collected by us in any way. -If you choose to select the option to keep you logged in, you understand that your sign-in details may be stored locally, but not in any way shared with us. These details are stored in the application data and we hold no responsibility for ensuring their concealment. +These terms and conditions ("Terms", "Agreement") are an agreement between NIAEFEUP ("us", "we" or " +our"), Núcleo de Informática da Associação de Estudantes da Universidade do Porto, and you (" +User", "you" or "your"). This Agreement sets forth the general terms and conditions of your use of +the Uni Mobile Application and any of its products or services (collectively, "Mobile Application" +or "Services"). \ \ -**User content** +**Accounts and Membership** -We do not own any data, information or material ("Content") that you submit in the Mobile Application in the course of using the Service. You shall have sole responsibility for the accuracy, quality, integrity, legality, reliability, appropriateness, and intellectual property ownership or right to use of all submitted Content. We may, but have no obligation to, monitor and review Content in the Mobile Application submitted or created using our Services by you. Unless specifically permitted by you, your use of the Mobile Application does not grant us the license to use, reproduce, adapt, modify, publish or distribute the Content created by you or stored in your user account for commercial, marketing or any similar purpose. But you grant us permission to access, copy, distribute, store, transmit, reformat, display and perform the Content of your user account solely as required for the purpose of providing the Services to you. Without limiting any of those representations or warranties, we have the right, though not the obligation, to, in our own sole discretion, refuse or remove any Content that, in our reasonable opinion, violates any of our policies or is in any way harmful or objectionable. +To use the Uni Mobile Application, you are required to hold a digital account provided by +Universidade +do Porto. We are in no way affiliated with this university. Any act performed by you through the +mobile application is subject to +the [Universidade do Porto's terms and conditions](https://sigarra.up.pt/up/pt/web_base.gera_pagina?p_pagina=termos%20e%20condicoes). +By using your faculty account in the Mobile Application, you are responsible for maintaining the +security of your account and you are fully responsible for all activities that occur under the +account and any other actions taken in connection with it. We will not be liable for any acts or +omissions by you, including any damages of any kind incurred as a result of such acts or omissions. +Any interaction with your data from Universidade do Porto's platform is made directly through it, +and is not collected by us in any way. +If you choose to select the option to keep you logged in, you understand that your sign-in details +may be stored locally, but not in any way shared with us. These details are stored in the +application data and we hold no responsibility for ensuring their concealment. +\ +\ +**Ownership of Content** + +We do not own any data, information or material ("Content") that you submit in the Mobile +Application in the course of using the Service. You shall have sole responsibility for the accuracy, +quality, integrity, legality, reliability, appropriateness, and intellectual property ownership or +right to use of all submitted Content. \ \ -**Backups** +**Data usage, Monitoring and Privacy** -We are not responsible for Content residing in the Mobile Application. In no event shall we be held liable for any loss of any Content. It is your sole responsibility to maintain appropriate backup of your Content. +We may, at our discretion but with no obligation, monitor and review Content in the Mobile +Application submitted or created using our Services by you. +Personal data of users is not collected for any other purpose, unless providing the Services to you, +except in certain circumstances described +in [Play Store’s Privacy Policy](https://support.google.com/googleplay/android-developer/answer/10144311?visit_id=638365485539535125-3072678242&rd=1) +and [App Store's Privacy Policy](https://developer.apple.com/app-store/app-privacy-details/#user-tracking). \ \ -**Links to other mobile applications** +**License and Usage Restrictions:** -Although this Mobile Application may link to other mobile applications, we are not, directly or indirectly, implying any approval, association, sponsorship, endorsement, or affiliation with any linked mobile application, unless specifically stated herein. Some of the links in the Mobile Application may be "affiliate links". This means if you click on the link and purchase an item, NIAEFEUP will receive an affiliate commission. We are not responsible for examining or evaluating, and we do not warrant the offerings of, any businesses or individuals or the content of their mobile applications. We do not assume any responsibility or liability for the actions, products, services, and content of any other third-parties. You should carefully review the legal statements and other conditions of use of any mobile application which you access through a link from this Mobile Application. Your linking to any other off-site mobile applications is at your own risk. +You grant us permission to access, copy, distribute, store, transmit, reformat, display and perform +the Content of your user account solely as required for the purpose of providing the Services to +you. +Unless specifically permitted by you, your use of the Mobile Application does not grant us the +license to link, reproduce, adapt, modify, publish or distribute the Content created by you or +stored in your user account, for evaluating the effectiveness of existing product features, planning +new features, or measuring size and characteristics about specific end-users, or devices with a data +broker, for commercial, marketing or any similar purpose. +Unless specifically permitted by you, your +use of the Mobile Application does not grant us the +license to use analytics tools, advertising networks, third-party SDKs, or other external vendors +whose code we have added to the Uni Mobile Application, to collect information such as user ID, +device ID, or profiles with Third-Party Data for the purpose of accessing necessary data to +implement +the Services we provide. We anonymously track page visits and widget clicks, which is shared with +our self-hosted instance of [plausible analytics](https://plausible.niaefeup.pt/). +we do not have the license to sell or share sensitive data collected with the purpose of +facilitating sales. +\ +\ +**Content Removal:** + +Without limiting any of those representations or warranties, we have the right, though not the +obligation, to, in our own sole discretion, refuse or remove any content that, in our reasonable +opinion, violates any of our policies or is in any way harmful or objectionable. \ \ -**Limitation of liability** +**Backups** -To the fullest extent permitted by applicable law, in no event will NIAEFEUP, its affiliates, officers, directors, employees, agents, suppliers or licensors be liable to any person for (a): any indirect, incidental, special, punitive, cover or consequential damages (including, without limitation, damages for lost profits, revenue, sales, goodwill, use of content, impact on business, business interruption, loss of anticipated savings, loss of business opportunity) however caused, under any theory of liability, including, without limitation, contract, tort, warranty, breach of statutory duty, negligence or otherwise, even if NIAEFEUP has been advised as to the possibility of such damages or could have foreseen such damages. To the maximum extent permitted by applicable law, the aggregate liability of NIAEFEUP and its affiliates, officers, employees, agents, suppliers and licensors, relating to the services will be limited to an amount greater of one dollar or any amounts actually paid in cash by you to NIAEFEUP for the prior one month period prior to the first event or occurrence giving rise to such liability. The limitations and exclusions also apply if this remedy does not fully compensate you for any losses or fails of its essential purpose. +We are not responsible for Content residing in the Mobile Application. In no event shall we be held +liable for any loss of any Content. It is your sole responsibility to maintain appropriate backup of +your Content. \ \ -**Changes and amendments** +**Links to Other Mobile Applications** + +Although this Mobile Application may link to other mobile applications, we are not, directly or +indirectly, implying any approval, association, sponsorship, endorsement, or affiliation with any +linked mobile application, unless specifically stated herein. Some of the links in the Mobile +Application may be "affiliate links". This means if you click on the link and purchase an item, +NIAEFEUP will receive an affiliate commission. We are not responsible for examining or evaluating, +and we do not warrant the offerings of, any businesses or individuals or the content of their mobile +applications. We do not assume any responsibility or liability for the actions, products, Services, +and Content of any other Third-Parties. You should carefully review the legal statements and other +conditions of use of any Mobile Application which you access through a link from this Mobile +Application. Your linking to any other off-site Mobile Applications is at your own risk. +\ +\ +**Limitation of Liability** + +To the fullest extent permitted by applicable law, in no event will NIAEFEUP, its affiliates, +officers, directors, employees, agents, suppliers or licensors be liable to any person for (a): any +indirect, incidental, special, punitive, cover or consequential damages (including, without +limitation, damages for lost profits, revenue, sales, goodwill, use of content, impact on business, +business interruption, loss of anticipated savings, loss of business opportunity) however caused, +under any theory of liability, including, without limitation, contract, tort, warranty, breach of +statutory duty, negligence or otherwise, even if NIAEFEUP has been advised as to the possibility of +such damages or could have foreseen such damages. To the maximum extent permitted by applicable law, +the aggregate liability of NIAEFEUP and its affiliates, officers, employees, agents, suppliers and +licensors, relating to the services will be limited to an amount greater of one dollar or any +amounts actually paid in cash by you to NIAEFEUP for the prior one month period prior to the first +event or occurrence giving rise to such liability. The limitations and exclusions also apply if this +remedy does not fully compensate you for any losses or fails of its essential purpose. +\ +\ +**Changes and Amendments** -We reserve the right to modify this Agreement or its policies relating to the Mobile Application or Services at any time, effective upon posting of an updated version of this Agreement in the Mobile Application. When we do, we will revise the updated date at the bottom of this page. Continued use of the Mobile Application after any such changes shall constitute your consent to such changes. Policy was created with [Website Policies](https://www.websitepolicies.com). +We reserve the right to modify this Agreement or its policies relating to the Mobile Application or +services at any time, effective upon posting of an updated version of this Agreement in the Mobile +Application. When we do, we will revise the updated date at the bottom of this page. Continued use +of the Mobile Application after any such changes will only constitute if, when prompted, you consent +to them. Otherwise, you will be logged out of the Mobile Application. +This policy was created with [Website Policies](https://www.websitepolicies.com). \ \ -**Acceptance of these terms** +**Acceptance of These Terms** -You acknowledge that you have read this Agreement and agree to all its terms and conditions. By using the Mobile Application or its Services you agree to be bound by this Agreement. If you do not agree to abide by the terms of this Agreement, you are not authorized to use or access the Mobile Application and its Services. +You acknowledge that you have read this Agreement and agree to all its terms and conditions. By +using the Mobile Application or its Services you agree to be bound by this Agreement. If you do not +agree to abide by the terms of this Agreement, you are not authorized to use or access the Mobile +Application and its Services. \ \ -**Contacting us** +**Contacting Us** -If you would like to contact us to understand more about this Agreement or wish to contact us concerning any matter relating to it, you may send an email to [ni@aefeup.pt](mailto:ni@aefeup.pt). +If you would like to contact us to understand more about this Agreement or wish to contact us +concerning any matter relating to it, you may send an email to [ni@aefeup.pt](mailto:ni@aefeup.pt). \ \ -This document was last updated on February 25, 2021 +This document was last updated on February 28, 2023. diff --git a/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/uni/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/uni/ios/RunnerTests/RunnerTests.swift b/uni/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/uni/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index b09b0365a..4657dee45 100644 --- a/uni/lib/controller/background_workers/notifications.dart +++ b/uni/lib/controller/background_workers/notifications.dart @@ -5,10 +5,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:logger/logger.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/background_workers/notifications/tuition_notification.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/notification_timeout_storage.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/session.dart'; import 'package:workmanager/workmanager.dart'; @@ -68,8 +69,9 @@ class NotificationManager { static const Duration _notificationWorkerPeriod = Duration(hours: 1); static Future updateAndTriggerNotifications() async { - final userInfo = await AppSharedPreferences.getPersistentUserInfo(); - final faculties = await AppSharedPreferences.getUserFaculties(); + PreferencesController.prefs = await SharedPreferences.getInstance(); + final userInfo = PreferencesController.getPersistentUserInfo(); + final faculties = PreferencesController.getUserFaculties(); if (userInfo == null || faculties.isEmpty) { return; diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index 738b5ce24..44411dabb 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -2,7 +2,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/controller/background_workers/notifications.dart'; import 'package:uni/controller/fetchers/fees_fetcher.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/utils/duration_string_formatter.dart'; @@ -53,7 +53,7 @@ class TuitionNotification extends Notification { @override Future shouldDisplay(Session session) async { final notificationsAreDisabled = - !(await AppSharedPreferences.getTuitionNotificationToggle()); + !PreferencesController.getTuitionNotificationToggle(); if (notificationsAreDisabled) return false; final feesFetcher = FeesFetcher(); final dueDate = parseFeesNextLimit( diff --git a/uni/lib/controller/cleanup.dart b/uni/lib/controller/cleanup.dart index 11bea65d2..8b8c519db 100644 --- a/uni/lib/controller/cleanup.dart +++ b/uni/lib/controller/cleanup.dart @@ -4,22 +4,22 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; -import 'package:uni/controller/local_storage/app_course_units_database.dart'; -import 'package:uni/controller/local_storage/app_courses_database.dart'; -import 'package:uni/controller/local_storage/app_exams_database.dart'; -import 'package:uni/controller/local_storage/app_last_user_info_update_database.dart'; -import 'package:uni/controller/local_storage/app_lectures_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/database/app_bus_stop_database.dart'; +import 'package:uni/controller/local_storage/database/app_course_units_database.dart'; +import 'package:uni/controller/local_storage/database/app_courses_database.dart'; +import 'package:uni/controller/local_storage/database/app_exams_database.dart'; +import 'package:uni/controller/local_storage/database/app_last_user_info_update_database.dart'; +import 'package:uni/controller/local_storage/database/app_lectures_database.dart'; +import 'package:uni/controller/local_storage/database/app_user_database.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/providers/state_providers.dart'; Future cleanupStoredData(BuildContext context) async { - StateProviders.fromContext(context).markAsNotInitialized(); + StateProviders.fromContext(context).invalidate(); final prefs = await SharedPreferences.getInstance(); - final faculties = await AppSharedPreferences.getUserFaculties(); + final faculties = PreferencesController.getUserFaculties(); await prefs.clear(); unawaited( @@ -41,3 +41,38 @@ Future cleanupStoredData(BuildContext context) async { directory.deleteSync(recursive: true); } } + +Future cleanupCachedFiles() async { + final lastCleanupDate = PreferencesController.getLastCleanUpDate(); + final daysSinceLastCleanup = + DateTime.now().difference(lastCleanupDate).inDays; + + if (daysSinceLastCleanup < 14) { + return; + } + + final toCleanDirectory = await getApplicationDocumentsDirectory(); + final threshold = DateTime.now().subtract(const Duration(days: 30)); + final directories = toCleanDirectory.listSync(followLinks: false); + + for (final directory in directories) { + if (directory is Directory) { + final files = directory.listSync(recursive: true, followLinks: false); + + final oldFiles = files.where((file) { + try { + final fileDate = File(file.path).lastModifiedSync(); + return fileDate.isBefore(threshold); + } catch (e) { + return false; + } + }); + + for (final file in oldFiles) { + await File(file.path).delete(); + } + } + } + + await PreferencesController.setLastCleanUpDate(DateTime.now()); +} diff --git a/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart index c84932de1..ef762fc80 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart @@ -10,18 +10,22 @@ class AllCourseUnitsFetcher { Session session, { List? currentCourseUnits, }) async { - final allCourseUnits = []; - - for (final course in courses) { - final courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( - course, - session, - currentCourseUnits: currentCourseUnits, - ); - allCourseUnits.addAll(courseUnits.where((c) => c.enrollmentIsValid())); - } + final courseCourseUnits = await Future.wait( + courses + .map( + (course) => _getAllCourseUnitsAndCourseAveragesFromCourse( + course, + session, + currentCourseUnits: currentCourseUnits, + ), + ) + .toList(), + ); - return allCourseUnits; + return courseCourseUnits + .expand((l) => l) + .where((c) => c.enrollmentIsValid()) + .toList(); } Future> _getAllCourseUnitsAndCourseAveragesFromCourse( diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart index 608a23f6c..1c54029e3 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -3,6 +3,7 @@ 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_directory.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; @@ -26,6 +27,27 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseCourseUnitSheet(response); } + Future> fetchCourseUnitFiles( + Session session, + int occurId, + ) async { + final url = '${getEndpoints(session)[0]}mob_ucurr_geral.conteudos'; + final response = await NetworkRouter.getWithCookies( + url, + { + 'pv_ocorrencia_id': occurId.toString(), + }, + session, + ); + return parseFiles(response, session); + } + + Future getDownloadLink( + Session session, + ) async { + return '${getEndpoints(session)[0]}conteudos_service.conteudos_cont'; + } + Future> fetchCourseUnitClasses( Session session, int occurrId, diff --git a/uni/lib/controller/fetchers/exam_fetcher.dart b/uni/lib/controller/fetchers/exam_fetcher.dart index 9231eff88..f29eb2c68 100644 --- a/uni/lib/controller/fetchers/exam_fetcher.dart +++ b/uni/lib/controller/fetchers/exam_fetcher.dart @@ -55,7 +55,6 @@ class ExamFetcher implements SessionDependantFetcher { } } } - return exams.toList(); } } diff --git a/uni/lib/controller/fetchers/faculties_fetcher.dart b/uni/lib/controller/fetchers/faculties_fetcher.dart new file mode 100644 index 000000000..f27c896bb --- /dev/null +++ b/uni/lib/controller/fetchers/faculties_fetcher.dart @@ -0,0 +1,33 @@ +import 'package:html/parser.dart'; +import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/model/entities/session.dart'; + +Future> getStudentFaculties(Session session) async { + final response = await NetworkRouter.getWithCookies( + 'https://sigarra.up.pt/up/pt/vld_entidades_geral.entidade_pagina', + {'pct_codigo': session.username}, + session, + ); + + final document = parse(response.body); + + final facultiesList = + document.querySelectorAll('#conteudoinner>ul a').map((e) => e.text); + + if (facultiesList.isEmpty) { + // The user is enrolled in a single faculty, + // and the selection page is skipped. + // We can extract the faculty from any anchor. + final singleFaculty = document.querySelector('a')!.attributes['href']!; + final uri = Uri.parse(singleFaculty); + final faculty = uri.pathSegments[0]; + return [faculty.toLowerCase()]; + } + + // We extract the faculties from the list. + // An example list is (201906166 (FEUP), 201906166 (FCUP)). + final regex = RegExp(r'.*\(([A-Z]+)\)'); + return facultiesList + .map((e) => regex.firstMatch(e)!.group(1)!.toLowerCase()) + .toList(); +} diff --git a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart index b9e6a8cce..b34d8a0fd 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart @@ -2,34 +2,51 @@ import 'package:uni/controller/fetchers/session_dependant_fetcher.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/utils/time/week.dart'; /// Class for fetching the user's schedule. abstract class ScheduleFetcher extends SessionDependantFetcher { // Returns the user's lectures. Future> getLectures(Session session, Profile profile); - /// Returns [Dates]. - Dates getDates() { - var date = DateTime.now(); + List getWeeks(DateTime now) { + final week = Week(start: now); - final beginWeek = date.year.toString().padLeft(4, '0') + - date.month.toString().padLeft(2, '0') + - date.day.toString().padLeft(2, '0'); - date = date.add(const Duration(days: 6)); + // In a 7-day period, there are at most 2 weeks. According to SIGARRA + // convention, weeks start on Sundays. + // Also, for `nextWeek`, we can't use `thisWeek.next()` because it could + // return a week that doesn't intersect `week` (if `week` starts on a + // Sunday). + final thisWeek = week.endingOn(DateTime.sunday); + final nextWeek = week.startingOn(DateTime.sunday); - final endWeek = date.year.toString().padLeft(4, '0') + - date.month.toString().padLeft(2, '0') + - date.day.toString().padLeft(2, '0'); + return thisWeek == nextWeek ? [thisWeek] : [thisWeek, nextWeek]; + } + + /// Returns [Dates]. + List getDates() { + final date = DateTime.now(); + final weeks = getWeeks(date); final lectiveYear = date.month < 8 ? date.year - 1 : date.year; - return Dates(beginWeek, endWeek, lectiveYear); + + return weeks.map((week) => Dates(week, lectiveYear)).toList(); } } /// Stores the start and end dates of the week and the current lective year. class Dates { - Dates(this.beginWeek, this.endWeek, this.lectiveYear); - final String beginWeek; - final String endWeek; + Dates(this.week, this.lectiveYear); + final Week week; final int lectiveYear; + + String _toSigarraDate(DateTime date) { + return date.year.toString().padLeft(4, '0') + + date.month.toString().padLeft(2, '0') + + date.day.toString().padLeft(2, '0'); + } + + String get asSigarraWeekStart => _toSigarraDate(week.start); + String get asSigarraWeekEnd => + _toSigarraDate(week.end.subtract(const Duration(days: 1))); } 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 d50099ff5..e0892fcce 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart @@ -1,10 +1,11 @@ -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_schedule.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/utils/time/week.dart'; /// Class for fetching the user's lectures from the faculties' API. class ScheduleFetcherApi extends ScheduleFetcher { @@ -20,20 +21,25 @@ class ScheduleFetcherApi extends ScheduleFetcher { @override Future> getLectures(Session session, Profile profile) async { final dates = getDates(); + final urls = getEndpoints(session); - final responses = []; + final responses = <(Week, http.Response)>[]; for (final url in urls) { - final response = await NetworkRouter.getWithCookies( - url, - { - 'pv_codigo': session.username, - 'pv_semana_ini': dates.beginWeek, - 'pv_semana_fim': dates.endWeek, - }, - session, + final futures = dates.map( + (date) => NetworkRouter.getWithCookies( + url, + { + 'pv_codigo': session.username, + 'pv_semana_ini': date.asSigarraWeekStart, + 'pv_semana_fim': date.asSigarraWeekEnd, + }, + session, + ).then((response) => (date.week, response)), ); - responses.add(response); + + responses.addAll(await Future.wait(futures)); } + return parseScheduleMultipleRequests(responses); } } diff --git a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart index e1117d81e..6ca596dd1 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart @@ -1,4 +1,4 @@ -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; @@ -6,6 +6,7 @@ import 'package:uni/controller/parsers/parser_schedule_html.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/utils/time/week.dart'; /// Class for fetching the user's lectures from the schedule's HTML page. class ScheduleFetcherHtml extends ScheduleFetcher { @@ -23,22 +24,27 @@ class ScheduleFetcherHtml extends ScheduleFetcher { final dates = getDates(); final baseUrls = NetworkRouter.getBaseUrlsFromSession(session); - final lectureResponses = >[]; + final lectureResponses = >[]; for (final baseUrl in baseUrls) { final url = '${baseUrl}hor_geral.estudantes_view'; for (final course in profile.courses) { - final response = await NetworkRouter.getWithCookies( - url, - { - 'pv_fest_id': course.festId.toString(), - 'pv_ano_lectivo': dates.lectiveYear.toString(), - 'p_semana_inicio': dates.beginWeek, - 'p_semana_fim': dates.endWeek, - }, - session, + final futures = dates.map( + (date) => NetworkRouter.getWithCookies( + url, + { + 'pv_fest_id': course.festId.toString(), + 'pv_ano_lectivo': date.lectiveYear.toString(), + 'p_semana_inicio': date.asSigarraWeekStart, + 'p_semana_fim': date.asSigarraWeekEnd, + }, + session, + ).then( + (response) => Tuple2((date.week, response), baseUrl), + ), ); - lectureResponses.add(Tuple2(response, baseUrl)); + + lectureResponses.addAll(await Future.wait(futures)); } } diff --git a/uni/lib/controller/load_static/terms_and_conditions.dart b/uni/lib/controller/fetchers/terms_and_conditions_fetcher.dart similarity index 55% rename from uni/lib/controller/load_static/terms_and_conditions.dart rename to uni/lib/controller/fetchers/terms_and_conditions_fetcher.dart index e5e5fc5da..151327da4 100644 --- a/uni/lib/controller/load_static/terms_and_conditions.dart +++ b/uni/lib/controller/fetchers/terms_and_conditions_fetcher.dart @@ -1,24 +1,14 @@ import 'dart:convert'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/services.dart' show rootBundle; -import 'package:http/http.dart' as http; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; /// 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 fetchTermsAndConditions() async { - if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - const url = - 'https://raw.githubusercontent.com/NIAEFEUP/project-schrodinger/develop/uni/assets/text/TermsAndConditions.md'; - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - return response.body; - } - } return rootBundle.loadString('assets/text/TermsAndConditions.md'); } @@ -29,27 +19,29 @@ Future fetchTermsAndConditions() async { /// or true if they haven't. /// Returns the updated value. Future updateTermsAndConditionsAcceptancePreference() async { - final hash = await AppSharedPreferences.getTermsAndConditionHash(); + final hash = PreferencesController.getTermsAndConditionHash(); final termsAndConditions = await fetchTermsAndConditions(); final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); if (hash == null) { - await AppSharedPreferences.setTermsAndConditionsAcceptance( + await PreferencesController.setTermsAndConditionsAcceptance( areAccepted: true, ); - await AppSharedPreferences.setTermsAndConditionHash(currentHash); + await PreferencesController.setTermsAndConditionHash(currentHash); return true; } if (currentHash != hash) { - await AppSharedPreferences.setTermsAndConditionsAcceptance( + await PreferencesController.setTermsAndConditionsAcceptance( areAccepted: false, ); - await AppSharedPreferences.setTermsAndConditionHash(currentHash); + await PreferencesController.setTermsAndConditionHash(currentHash); return false; } - await AppSharedPreferences.setTermsAndConditionsAcceptance(areAccepted: true); + await PreferencesController.setTermsAndConditionsAcceptance( + areAccepted: true, + ); return true; } @@ -57,6 +49,8 @@ Future updateTermsAndConditionsAcceptancePreference() async { Future acceptTermsAndConditions() async { final termsAndConditions = await fetchTermsAndConditions(); final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); - await AppSharedPreferences.setTermsAndConditionHash(currentHash); - await AppSharedPreferences.setTermsAndConditionsAcceptance(areAccepted: true); + await PreferencesController.setTermsAndConditionHash(currentHash); + await PreferencesController.setTermsAndConditionsAcceptance( + areAccepted: true, + ); } diff --git a/uni/lib/controller/local_storage/app_bus_stop_database.dart b/uni/lib/controller/local_storage/database/app_bus_stop_database.dart similarity index 97% rename from uni/lib/controller/local_storage/app_bus_stop_database.dart rename to uni/lib/controller/local_storage/database/app_bus_stop_database.dart index 7d53884d4..357aefd43 100644 --- a/uni/lib/controller/local_storage/app_bus_stop_database.dart +++ b/uni/lib/controller/local_storage/database/app_bus_stop_database.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; /// Manages the app's Bus Stops database. diff --git a/uni/lib/controller/local_storage/app_calendar_database.dart b/uni/lib/controller/local_storage/database/app_calendar_database.dart similarity index 93% rename from uni/lib/controller/local_storage/app_calendar_database.dart rename to uni/lib/controller/local_storage/database/app_calendar_database.dart index b8674e2b9..6e05891b6 100644 --- a/uni/lib/controller/local_storage/app_calendar_database.dart +++ b/uni/lib/controller/local_storage/database/app_calendar_database.dart @@ -1,4 +1,4 @@ -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/calendar_event.dart'; class CalendarDatabase extends AppDatabase { diff --git a/uni/lib/controller/local_storage/app_course_units_database.dart b/uni/lib/controller/local_storage/database/app_course_units_database.dart similarity index 96% rename from uni/lib/controller/local_storage/app_course_units_database.dart rename to uni/lib/controller/local_storage/database/app_course_units_database.dart index c9968845a..db82545f2 100644 --- a/uni/lib/controller/local_storage/app_course_units_database.dart +++ b/uni/lib/controller/local_storage/database/app_course_units_database.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; class AppCourseUnitsDatabase extends AppDatabase { diff --git a/uni/lib/controller/local_storage/app_courses_database.dart b/uni/lib/controller/local_storage/database/app_courses_database.dart similarity index 97% rename from uni/lib/controller/local_storage/app_courses_database.dart rename to uni/lib/controller/local_storage/database/app_courses_database.dart index db7d9ac6a..b459918e7 100644 --- a/uni/lib/controller/local_storage/app_courses_database.dart +++ b/uni/lib/controller/local_storage/database/app_courses_database.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/course.dart'; /// Manages the app's Courses database. diff --git a/uni/lib/controller/local_storage/app_database.dart b/uni/lib/controller/local_storage/database/app_database.dart similarity index 100% rename from uni/lib/controller/local_storage/app_database.dart rename to uni/lib/controller/local_storage/database/app_database.dart diff --git a/uni/lib/controller/local_storage/app_exams_database.dart b/uni/lib/controller/local_storage/database/app_exams_database.dart similarity index 86% rename from uni/lib/controller/local_storage/app_exams_database.dart rename to uni/lib/controller/local_storage/database/app_exams_database.dart index 6d86a01c7..8d53df3da 100644 --- a/uni/lib/controller/local_storage/app_exams_database.dart +++ b/uni/lib/controller/local_storage/database/app_exams_database.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/exam.dart'; /// Manages the app's Exams database. @@ -11,20 +11,6 @@ import 'package:uni/model/entities/exam.dart'; class AppExamsDatabase extends AppDatabase { AppExamsDatabase() : super('exams.db', [_createScript], onUpgrade: migrate, version: 5); - Map months = { - 'Janeiro': '01', - 'Fevereiro': '02', - 'Março': '03', - 'Abril': '04', - 'Maio': '05', - 'Junho': '06', - 'Julho': '07', - 'Agosto': '08', - 'Setembro': '09', - 'Outubro': '10', - 'Novembro': '11', - 'Dezembro': '12', - }; static const _createScript = ''' CREATE TABLE exams(id TEXT, subject TEXT, begin TEXT, end TEXT, diff --git a/uni/lib/controller/local_storage/app_last_user_info_update_database.dart b/uni/lib/controller/local_storage/database/app_last_user_info_update_database.dart similarity index 94% rename from uni/lib/controller/local_storage/app_last_user_info_update_database.dart rename to uni/lib/controller/local_storage/database/app_last_user_info_update_database.dart index 93af19039..e2d4656ac 100644 --- a/uni/lib/controller/local_storage/app_last_user_info_update_database.dart +++ b/uni/lib/controller/local_storage/database/app_last_user_info_update_database.dart @@ -1,4 +1,4 @@ -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; /// Manages the app's Last User Info Update database. /// diff --git a/uni/lib/controller/local_storage/app_lectures_database.dart b/uni/lib/controller/local_storage/database/app_lectures_database.dart similarity index 97% rename from uni/lib/controller/local_storage/app_lectures_database.dart rename to uni/lib/controller/local_storage/database/app_lectures_database.dart index 7430020e5..6327cec4c 100644 --- a/uni/lib/controller/local_storage/app_lectures_database.dart +++ b/uni/lib/controller/local_storage/database/app_lectures_database.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/lecture.dart'; /// Manages the app's Lectures database. diff --git a/uni/lib/controller/local_storage/app_library_occupation_database.dart b/uni/lib/controller/local_storage/database/app_library_occupation_database.dart similarity index 94% rename from uni/lib/controller/local_storage/app_library_occupation_database.dart rename to uni/lib/controller/local_storage/database/app_library_occupation_database.dart index a4ab4b35a..6f39d77c7 100644 --- a/uni/lib/controller/local_storage/app_library_occupation_database.dart +++ b/uni/lib/controller/local_storage/database/app_library_occupation_database.dart @@ -1,4 +1,4 @@ -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/library_occupation.dart'; class LibraryOccupationDatabase extends AppDatabase { diff --git a/uni/lib/controller/local_storage/app_references_database.dart b/uni/lib/controller/local_storage/database/app_references_database.dart similarity index 96% rename from uni/lib/controller/local_storage/app_references_database.dart rename to uni/lib/controller/local_storage/database/app_references_database.dart index 2693357a7..feff82385 100644 --- a/uni/lib/controller/local_storage/app_references_database.dart +++ b/uni/lib/controller/local_storage/database/app_references_database.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/reference.dart'; /// Manages the app's References database. diff --git a/uni/lib/controller/local_storage/app_restaurant_database.dart b/uni/lib/controller/local_storage/database/app_restaurant_database.dart similarity index 98% rename from uni/lib/controller/local_storage/app_restaurant_database.dart rename to uni/lib/controller/local_storage/database/app_restaurant_database.dart index 56663b513..de96b51d6 100644 --- a/uni/lib/controller/local_storage/app_restaurant_database.dart +++ b/uni/lib/controller/local_storage/database/app_restaurant_database.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/utils/day_of_week.dart'; diff --git a/uni/lib/controller/local_storage/app_user_database.dart b/uni/lib/controller/local_storage/database/app_user_database.dart similarity index 97% rename from uni/lib/controller/local_storage/app_user_database.dart rename to uni/lib/controller/local_storage/database/app_user_database.dart index 6bbd86d16..6fd14cf29 100644 --- a/uni/lib/controller/local_storage/app_user_database.dart +++ b/uni/lib/controller/local_storage/database/app_user_database.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:uni/controller/local_storage/app_database.dart'; +import 'package:uni/controller/local_storage/database/app_database.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; diff --git a/uni/lib/controller/local_storage/file_offline_storage.dart b/uni/lib/controller/local_storage/file_offline_storage.dart index 9580def16..dca50a64a 100644 --- a/uni/lib/controller/local_storage/file_offline_storage.dart +++ b/uni/lib/controller/local_storage/file_offline_storage.dart @@ -6,12 +6,11 @@ import 'package:http/http.dart' as http; import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uni/controller/networking/network_router.dart'; - import 'package:uni/model/entities/session.dart'; /// The offline image storage location on the device. Future get _localPath async { - final directory = await getTemporaryDirectory(); + final directory = await getApplicationDocumentsDirectory(); return directory.path; } @@ -67,9 +66,11 @@ Future _downloadAndSaveFile( Session? session, Map? headers, ) async { + final header = headers ?? {}; + final response = session == null ? await http.get(url.toUri(), headers: headers) - : await NetworkRouter.getWithCookies(url, {}, session); + : await NetworkRouter.getWithCookies(url, header, session); if (response.statusCode == 200) { return File(filePath).writeAsBytes(response.bodyBytes); diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/preferences_controller.dart similarity index 51% rename from uni/lib/controller/local_storage/app_shared_preferences.dart rename to uni/lib/controller/local_storage/preferences_controller.dart index 8c458cbd2..3a8ba7633 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/preferences_controller.dart @@ -13,36 +13,49 @@ import 'package:uni/utils/favorite_widget_type.dart'; /// /// This database stores the user's student number, password and favorite /// widgets. -class AppSharedPreferences { +class PreferencesController { + static late SharedPreferences prefs; + static final iv = encrypt.IV.fromBase64('jF9jjdSEPgsKnf0jCl1GAQ=='); static final key = encrypt.Key.fromBase64('DT3/GTNYldhwOD3ZbpVLoAwA/mncsN7U7sJxfFn3y0A='); - 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 = + 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 themeMode = 'theme_mode'; - static const String locale = 'app_locale'; - static const String favoriteCards = 'favorite_cards'; - static final List defaultFavoriteCards = [ + static const String _usageStatsToggleKey = 'usage_stats_toogle'; + static const String _themeMode = 'theme_mode'; + static const String _isDataCollectionBannerViewedKey = + 'data_collection_banner'; + static const String _locale = 'app_locale'; + static const String _lastCacheCleanUpDate = 'last_clean'; + static const String _favoriteCards = 'favorite_cards'; + static final List _defaultFavoriteCards = [ FavoriteWidgetType.schedule, FavoriteWidgetType.exams, FavoriteWidgetType.busStops, ]; - static const String hiddenExams = 'hidden_exams'; - static const String favoriteRestaurants = 'favorite_restaurants'; - static const String filteredExamsTypes = 'filtered_exam_types'; - static final List defaultFilteredExamTypes = Exam.displayedTypes; + static const String _hiddenExams = 'hidden_exams'; + static const String _favoriteRestaurants = 'favorite_restaurants'; + static const String _filteredExamsTypes = 'filtered_exam_types'; + static final List _defaultFilteredExamTypes = Exam.displayedTypes; + + static final _statsToggleStreamController = + StreamController.broadcast(); + static final onStatsToggle = _statsToggleStreamController.stream; + + static final _hiddenExamsChangeStreamController = + StreamController>.broadcast(); + static final onHiddenExamsChange = _hiddenExamsChangeStreamController.stream; /// 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); + static DateTime? getLastDataClassUpdateTime(String dataKey) { + final lastUpdateTime = prefs.getString(dataKey + _lastUpdateTimeKeySuffix); return lastUpdateTime != null ? DateTime.parse(lastUpdateTime) : null; } @@ -51,9 +64,8 @@ class AppSharedPreferences { String dataKey, DateTime dateTime, ) async { - final prefs = await SharedPreferences.getInstance(); await prefs.setString( - dataKey + lastUpdateTimeKeySuffix, + dataKey + _lastUpdateTimeKeySuffix, dateTime.toString(), ); } @@ -64,11 +76,10 @@ class AppSharedPreferences { String pass, List faculties, ) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(userNumber, user); - await prefs.setString(userPw, encode(pass)); + await prefs.setString(_userNumber, user); + await prefs.setString(_userPw, encode(pass)); await prefs.setStringList( - userFaculties, + _userFaculties, faculties, ); // Could be multiple faculties } @@ -77,58 +88,59 @@ class AppSharedPreferences { static Future setTermsAndConditionsAcceptance({ required bool areAccepted, }) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(areTermsAndConditionsAcceptedKey, areAccepted); + await prefs.setBool(_areTermsAndConditionsAcceptedKey, areAccepted); } /// Returns whether or not the Terms and Conditions have been accepted. - static Future areTermsAndConditionsAccepted() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(areTermsAndConditionsAcceptedKey) ?? false; + static bool areTermsAndConditionsAccepted() { + return prefs.getBool(_areTermsAndConditionsAcceptedKey) ?? false; + } + + static Future setDataCollectionBannerViewed({ + required bool isViewed, + }) async { + await prefs.setBool(_isDataCollectionBannerViewedKey, isViewed); + } + + static bool isDataCollectionBannerViewed() { + return prefs.getBool(_isDataCollectionBannerViewedKey) ?? false; } /// Returns the hash of the last Terms and Conditions that have /// been accepted by the user. - static Future getTermsAndConditionHash() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(termsAndConditions); + static String? getTermsAndConditionHash() { + return prefs.getString(_termsAndConditions); } /// Sets the hash of the Terms and Conditions that have been accepted /// by the user. static Future setTermsAndConditionHash(String hashed) async { - final prefs = await SharedPreferences.getInstance(); - return prefs.setString(termsAndConditions, hashed); + return prefs.setString(_termsAndConditions, hashed); } /// Gets current used theme mode. - static Future getThemeMode() async { - final prefs = await SharedPreferences.getInstance(); - return ThemeMode.values[prefs.getInt(themeMode) ?? ThemeMode.system.index]; + static ThemeMode getThemeMode() { + return ThemeMode.values[prefs.getInt(_themeMode) ?? ThemeMode.system.index]; } /// Set new app theme mode. static Future setThemeMode(ThemeMode thmMode) async { - final prefs = await SharedPreferences.getInstance(); - return prefs.setInt(themeMode, thmMode.index); + return prefs.setInt(_themeMode, thmMode.index); } /// Set app next theme mode. static Future setNextThemeMode() async { - final prefs = await SharedPreferences.getInstance(); - final themeIndex = (await getThemeMode()).index; - return prefs.setInt(themeMode, (themeIndex + 1) % 3); + final themeIndex = getThemeMode().index; + return prefs.setInt(_themeMode, (themeIndex + 1) % 3); } static Future setLocale(AppLocale appLocale) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(locale, appLocale.name); + await prefs.setString(_locale, appLocale.name); } - static Future getLocale() async { - final prefs = await SharedPreferences.getInstance(); + static AppLocale getLocale() { final appLocale = - prefs.getString(locale) ?? Platform.localeName.substring(0, 2); + prefs.getString(_locale) ?? Platform.localeName.substring(0, 2); return AppLocale.values.firstWhere( (e) => e.toString() == 'AppLocale.$appLocale', @@ -136,11 +148,20 @@ class AppSharedPreferences { ); } + static Future setLastCleanUpDate(DateTime date) async { + await prefs.setString(_lastCacheCleanUpDate, date.toString()); + } + + static DateTime getLastCleanUpDate() { + final date = + prefs.getString(_lastCacheCleanUpDate) ?? DateTime.now().toString(); + return DateTime.parse(date); + } + /// Deletes the user's student number and password. static Future removePersistentUserInfo() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove(userNumber); - await prefs.remove(userPw); + await prefs.remove(_userNumber); + await prefs.remove(_userPw); } /// Returns a tuple containing the user's student number and password. @@ -149,9 +170,9 @@ class AppSharedPreferences { /// * the first element in the tuple is the user's student number. /// * the second element in the tuple is the user's password, in plain text /// format. - static Future?> getPersistentUserInfo() async { - final userNum = await getUserNumber(); - final userPass = await getUserPassword(); + static Tuple2? getPersistentUserInfo() { + final userNum = getUserNumber(); + final userPass = getUserPassword(); if (userNum == null || userPass == null) { return null; } @@ -159,23 +180,20 @@ class AppSharedPreferences { } /// Returns the user's faculties - static Future> getUserFaculties() async { - final prefs = await SharedPreferences.getInstance(); - final storedFaculties = prefs.getStringList(userFaculties); + static List getUserFaculties() { + final storedFaculties = prefs.getStringList(_userFaculties); return storedFaculties ?? ['feup']; // TODO(bdmendes): Store dropdown choices in the db for later storage; } /// Returns the user's student number. - static Future getUserNumber() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(userNumber); + static String? getUserNumber() { + return prefs.getString(_userNumber); } /// Returns the user's password, in plain text format. - static Future getUserPassword() async { - final prefs = await SharedPreferences.getInstance(); - final password = prefs.getString(userPw); + static String? getUserPassword() { + final password = prefs.getString(_userPw); return password != null ? decode(password) : null; } @@ -183,25 +201,23 @@ class AppSharedPreferences { static Future saveFavoriteCards( List newFavorites, ) async { - final prefs = await SharedPreferences.getInstance(); await prefs.setStringList( - favoriteCards, + _favoriteCards, newFavorites.map((a) => a.index.toString()).toList(), ); } /// Returns a list containing the user's favorite widgets. - static Future> getFavoriteCards() async { - final prefs = await SharedPreferences.getInstance(); + static List getFavoriteCards() { final storedFavorites = prefs - .getStringList(favoriteCards) + .getStringList(_favoriteCards) ?.where( (element) => int.parse(element) < FavoriteWidgetType.values.length, ) .toList(); if (storedFavorites == null) { - return defaultFavoriteCards; + return _defaultFavoriteCards; } return storedFavorites @@ -212,25 +228,22 @@ class AppSharedPreferences { static Future saveFavoriteRestaurants( List newFavoriteRestaurants, ) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(favoriteRestaurants, newFavoriteRestaurants); + await prefs.setStringList(_favoriteRestaurants, newFavoriteRestaurants); } - static Future> getFavoriteRestaurants() async { - final prefs = await SharedPreferences.getInstance(); + static List getFavoriteRestaurants() { final storedFavoriteRestaurants = - prefs.getStringList(favoriteRestaurants) ?? []; + prefs.getStringList(_favoriteRestaurants) ?? []; return storedFavoriteRestaurants; } static Future saveHiddenExams(List newHiddenExams) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(hiddenExams, newHiddenExams); + await prefs.setStringList(_hiddenExams, newHiddenExams); + _hiddenExamsChangeStreamController.add(newHiddenExams); } - static Future> getHiddenExams() async { - final prefs = await SharedPreferences.getInstance(); - final storedHiddenExam = prefs.getStringList(hiddenExams) ?? []; + static List getHiddenExams() { + final storedHiddenExam = prefs.getStringList(_hiddenExams) ?? []; return storedHiddenExam; } @@ -238,24 +251,21 @@ class AppSharedPreferences { static Future saveFilteredExams( Map newFilteredExamTypes, ) async { - final prefs = await SharedPreferences.getInstance(); - final newTypes = newFilteredExamTypes.keys .where((type) => newFilteredExamTypes[type] ?? false) .toList(); - await prefs.setStringList(filteredExamsTypes, newTypes); + await prefs.setStringList(_filteredExamsTypes, newTypes); } /// Returns the user's exam filter settings. - static Future> getFilteredExams() async { - final prefs = await SharedPreferences.getInstance(); - final storedFilteredExamTypes = prefs.getStringList(filteredExamsTypes); + static Map getFilteredExams() { + final storedFilteredExamTypes = prefs.getStringList(_filteredExamsTypes); if (storedFilteredExamTypes == null) { - return Map.fromIterable(defaultFilteredExamTypes, value: (type) => true); + return Map.fromIterable(_defaultFilteredExamTypes, value: (type) => true); } return Map.fromIterable( - defaultFilteredExamTypes, + _defaultFilteredExamTypes, value: storedFilteredExamTypes.contains, ); } @@ -282,15 +292,24 @@ class AppSharedPreferences { return encrypt.Encrypter(encrypt.AES(key)); } - static Future getTuitionNotificationToggle() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(tuitionNotificationsToggleKey) ?? true; + static bool getTuitionNotificationToggle() { + return prefs.getBool(_tuitionNotificationsToggleKey) ?? true; } static Future setTuitionNotificationToggle({ required bool value, }) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(tuitionNotificationsToggleKey, value); + await prefs.setBool(_tuitionNotificationsToggleKey, value); + } + + static bool getUsageStatsToggle() { + return prefs.getBool(_usageStatsToggleKey) ?? true; + } + + static Future setUsageStatsToggle({ + required bool value, + }) async { + await prefs.setBool(_usageStatsToggleKey, value); + _statsToggleStreamController.add(value); } } diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index 6b2ee56d3..3ff18efb9 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -5,7 +5,7 @@ import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:logger/logger.dart'; import 'package:synchronized/synchronized.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/view/navigation_service.dart'; @@ -36,20 +36,22 @@ class NetworkRouter { static Session? _cachedSession; /// Performs a login using the Sigarra API, - /// returning an authenticated [Session] on the given [faculties] with the + /// returning an authenticated [Session] with the /// given username [username] and password [password] if successful. static Future login( String username, String password, List faculties, { required bool persistentSession, + bool ignoreCached = false, }) async { return _loginLock.synchronized( () async { if (_lastLoginTime != null && DateTime.now().difference(_lastLoginTime!) < const Duration(minutes: 1) && - _cachedSession != null) { + _cachedSession != null && + !ignoreCached) { Logger().d('Login request ignored due to recent login'); return _cachedSession; } @@ -92,7 +94,7 @@ class NetworkRouter { /// returning an updated Session if successful. static Future reLoginFromSession(Session session) async { final username = session.username; - final password = await AppSharedPreferences.getUserPassword(); + final password = PreferencesController.getUserPassword(); if (password == null) { Logger().e('Re-login failed: password not found'); diff --git a/uni/lib/controller/networking/url_launcher.dart b/uni/lib/controller/networking/url_launcher.dart new file mode 100644 index 000000000..f4ff85ae5 --- /dev/null +++ b/uni/lib/controller/networking/url_launcher.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/common_widgets/toast_message.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future launchUrlWithToast(BuildContext context, String url) async { + final validUrl = Uri.parse(url); + if (url != '' && canLaunchUrl(validUrl) as bool) { + await launchUrl(Uri.parse(url)); + } else { + await ToastMessage.error( + context, + S.of(context).no_link, + ); + } +} diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 6ef395a2f..ad8751b19 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -1,7 +1,46 @@ +import 'dart:convert'; + import 'package:html/parser.dart'; import 'package:http/http.dart' as http; +import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/entities/session.dart'; + +Future> parseFiles( + http.Response response, + Session session, +) async { + final json = jsonDecode(response.body) as List; + final dirs = []; + if (json.isEmpty) return []; + + for (var item in json) { + item = item as Map; + final files = []; + for (final file in item['ficheiros'] as List) { + if (file is Map) { + final fileName = file['nome']; + final fileDate = file['data_actualizacao']; + final fileCode = file['codigo'].toString(); + final format = file['filename'] + .toString() + .substring(file['filename'].toString().indexOf('.')); + final url = await CourseUnitsInfoFetcher().getDownloadLink(session); + final courseUnitFile = CourseUnitFile( + '${fileName}_$fileDate$format', + url, + fileCode, + ); + files.add(courseUnitFile); + } + } + dirs.add(CourseUnitFileDirectory(item['nome'].toString(), files)); + } + return dirs; +} Future parseCourseUnitSheet(http.Response response) async { final document = parse(response.body); diff --git a/uni/lib/controller/parsers/parser_course_units.dart b/uni/lib/controller/parsers/parser_course_units.dart index b7531a44b..6843722b9 100644 --- a/uni/lib/controller/parsers/parser_course_units.dart +++ b/uni/lib/controller/parsers/parser_course_units.dart @@ -3,7 +3,6 @@ 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_units/course_unit.dart'; -import 'package:uni/utils/url_parser.dart'; List parseCourseUnitsAndCourseAverage( http.Response response, @@ -48,9 +47,9 @@ List parseCourseUnitsAndCourseAverage( final year = row.children[0].innerHtml; final semester = row.children[1].innerHtml; - final occurId = getUrlQueryParameters( + final occurId = Uri.parse( row.children[2].firstChild!.attributes['href']!, - )['pv_ocorrencia_id']!; + ).queryParameters['pv_ocorrencia_id']!; final codeName = row.children[2].children[0].innerHtml; final name = row.children[3].children[0].innerHtml; final ects = row.children[5].innerHtml.replaceAll(',', '.'); diff --git a/uni/lib/controller/parsers/parser_courses.dart b/uni/lib/controller/parsers/parser_courses.dart index 10b1e6d97..8e84304e3 100644 --- a/uni/lib/controller/parsers/parser_courses.dart +++ b/uni/lib/controller/parsers/parser_courses.dart @@ -1,7 +1,6 @@ import 'package:html/parser.dart' show parse; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course.dart'; -import 'package:uni/utils/url_parser.dart'; List parseMultipleCourses(List responses) { final courses = []; @@ -27,7 +26,7 @@ List _parseCourses(http.Response response) { final courseUrl = div .querySelector('.estudante-lista-curso-nome > a') ?.attributes['href']; - final courseId = getUrlQueryParameters(courseUrl ?? '')['pv_curso_id']; + final courseId = Uri.parse(courseUrl ?? '').queryParameters['pv_curso_id']; final courseState = div.querySelectorAll('.formulario td')[3].text; final courseFestId = div .querySelector('.estudante-lista-curso-detalhes > a') @@ -54,15 +53,15 @@ List _parseCourses(http.Response response) { final div = oldCourses[i]; final courseName = div.children[0].firstChild?.text?.trim(); final courseUrl = div.querySelector('a')?.attributes['href']; - final courseId = getUrlQueryParameters(courseUrl ?? '')['pv_curso_id']; + final courseId = Uri.parse(courseUrl ?? '').queryParameters['pv_curso_id']; var courseFirstEnrollment = div.children[4].text; courseFirstEnrollment = courseFirstEnrollment .substring(0, courseFirstEnrollment.indexOf('/')) .trim(); final courseState = div.children[5].text; - final courseFestId = getUrlQueryParameters( + final courseFestId = Uri.parse( div.children[6].firstChild?.attributes['href'] ?? '', - )['pv_fest_id']; + ).queryParameters['pv_fest_id']; courses.add( Course( firstEnrollment: int.parse(courseFirstEnrollment), diff --git a/uni/lib/controller/parsers/parser_exams.dart b/uni/lib/controller/parsers/parser_exams.dart index 6c4cd8875..c018c2aef 100644 --- a/uni/lib/controller/parsers/parser_exams.dart +++ b/uni/lib/controller/parsers/parser_exams.dart @@ -27,7 +27,6 @@ class ParserExams { final examTypes = []; var rooms = []; String? subject; - String? schedule; var id = '0'; var days = 0; var tableNum = 0; @@ -51,18 +50,24 @@ class ParserExams { .queryParameters['p_exa_id']!; } if (examsDay.querySelector('span.exame-sala') != null) { - rooms = - examsDay.querySelector('span.exame-sala')!.text.split(','); + rooms = examsDay + .querySelector('span.exame-sala')! + .text + .split(',') + .map((e) => e.trim()) + .toList(); + } + final DateTime begin; + final DateTime end; + if (!examsDay.text.endsWith('-')) { + final rx = RegExp(r'(\d{2}:\d{2})-(\d{2}:\d{2})'); + final match = rx.allMatches(examsDay.text).first; + begin = DateTime.parse('${dates[days]} ${match.group(1)!}'); + end = DateTime.parse('${dates[days]} ${match.group(2)!}'); + } else { + begin = DateTime.parse('${dates[days]} 00:00'); + end = DateTime.parse('${dates[days]} 00:00'); } - schedule = examsDay.text.substring( - examsDay.text.indexOf(':') - 2, - examsDay.text.indexOf(':') + 9, - ); - final splittedSchedule = schedule!.split('-'); - final begin = - DateTime.parse('${dates[days]} ${splittedSchedule[0]}'); - final end = - DateTime.parse('${dates[days]} ${splittedSchedule[1]}'); final exam = Exam( id, begin, @@ -72,7 +77,6 @@ class ParserExams { examTypes[tableNum], course.faculty!, ); - examsList.add(exam); }); } diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 0a950c7d0..7bedade65 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -1,16 +1,16 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:http/http.dart'; import 'package:uni/model/entities/lecture.dart'; -import 'package:uni/model/entities/time_utilities.dart'; +import 'package:uni/model/utils/time/week.dart'; +import 'package:uni/model/utils/time/weekday_mapper.dart'; Future> parseScheduleMultipleRequests( - List responses, + List<(Week, http.Response)> responsesPerWeeks, ) async { var lectures = []; - for (final response in responses) { - lectures += await parseSchedule(response); + for (final (week, response) in responsesPerWeeks) { + lectures += await parseSchedule(response, week); } return lectures; } @@ -19,7 +19,10 @@ Future> parseScheduleMultipleRequests( /// date. /// /// This function parses a JSON object. -Future> parseSchedule(http.Response response) async { +Future> parseSchedule( + http.Response response, + Week week, +) async { final lectures = {}; final json = jsonDecode(response.body) as Map; @@ -27,9 +30,15 @@ Future> parseSchedule(http.Response response) async { final schedule = json['horario'] as List; for (var lecture in schedule) { lecture = lecture as Map; - final day = ((lecture['dia'] as int) - 2) % - 7; // Api: monday = 2, Lecture.dart class: monday = 0 - final secBegin = lecture['hora_inicio'] as int; + + final startTime = week + .getWeekday(WeekdayMapper.fromSigarraToDart.map(lecture['dia'] as int)) + .add( + Duration( + seconds: lecture['hora_inicio'] as int, + ), + ); + final subject = lecture['ucurr_sigla'] as String; final typeClass = lecture['tipo'] as String; @@ -47,12 +56,10 @@ Future> parseSchedule(http.Response response) async { final classNumber = lecture['turma_sigla'] as String; final occurrId = lecture['ocorrencia_id'] as int; - final monday = DateTime.now().getClosestMonday(); - final lec = Lecture.fromApi( subject, typeClass, - monday.add(Duration(days: day, seconds: secBegin)), + startTime, blocks, room, teacher, diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index 6843f749d..4de3e04fd 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -6,17 +6,17 @@ import 'package:http/http.dart' as http; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/entities/time_utilities.dart'; +import 'package:uni/model/utils/time/week.dart'; +import 'package:uni/model/utils/time/weekday_mapper.dart'; Future> getOverlappedClasses( Session session, Document document, String faculty, + Week week, ) async { final lecturesList = []; - final monday = DateTime.now().getClosestMonday(); - final overlappingClasses = document.querySelectorAll('.dados > tbody > .d'); for (final element in overlappingClasses) { final subject = element.querySelector('acronym > a')?.text; @@ -31,7 +31,11 @@ Future> getOverlappedClasses( .querySelector('.horario > tbody > tr:first-child') ?.children .indexWhere((element) => element.text == textDay); - final day = aDay != null ? aDay - 1 : 0; + final day = week.getWeekday( + aDay != null + ? WeekdayMapper.fromSigarraToDart.map(aDay) + : DateTime.monday, + ); final startTime = element.querySelector('td[headers=t3]')?.text; final room = element.querySelector('td[headers=t4] > a')?.text; final teacher = element.querySelector('td[headers=t5] > a')?.text; @@ -44,13 +48,13 @@ Future> getOverlappedClasses( 'Overlapping class $subject has invalid startTime', ); } - final fullStartTime = monday.add( + final fullStartTime = day.add( Duration( - days: day, hours: int.parse(startTimeList[0]), minutes: int.parse(startTimeList[1]), ), ); + final href = element.querySelector('td[headers=t6] > a')?.attributes['href']; @@ -63,8 +67,11 @@ Future> getOverlappedClasses( session, ); - final classLectures = - await getScheduleFromHtml(response, session, faculty); + final classLectures = await getScheduleFromHtml( + (week, response), + session, + faculty, + ); lecturesList.add( classLectures @@ -79,7 +86,7 @@ Future> getOverlappedClasses( final lect = Lecture.fromHtml( subject!, typeClass!, - monday.add(Duration(days: day)), + day, startTime!, 0, room!, @@ -94,31 +101,31 @@ Future> getOverlappedClasses( return lecturesList; } -/// Extracts the user's lectures from an HTTP [response] and sorts them by date. +/// Extracts the user's lectures from a Week-HTTP pair in [responsePerWeek] and +/// sorts them by date. /// /// This function parses the schedule's HTML page. Future> getScheduleFromHtml( - http.Response response, + (Week, http.Response) responsePerWeek, Session session, String faculty, ) async { + final (week, response) = responsePerWeek; final document = parse(response.body); var semana = [0, 0, 0, 0, 0, 0]; final lecturesList = []; - final monday = DateTime.now().getClosestMonday(); - document.querySelectorAll('.horario > tbody > tr').forEach((Element element) { if (element.getElementsByClassName('horas').isNotEmpty) { - var day = 0; + var dayIndex = 0; final children = element.children; for (var i = 1; i < children.length; i++) { - for (var d = day; d < semana.length; d++) { + for (var d = dayIndex; d < semana.length; d++) { if (semana[d] == 0) { break; } - day++; + dayIndex++; } final clsName = children[i].className; if (clsName == 'TE' || clsName == 'TP' || clsName == 'PL') { @@ -139,12 +146,14 @@ Future> getScheduleFromHtml( final blocks = int.parse(children[i].attributes['rowspan']!); final startTime = children[0].text.substring(0, 5); - semana[day] += blocks; + semana[dayIndex] += blocks; final lect = Lecture.fromHtml( subject, typeClass, - monday.add(Duration(days: day)), + week.getWeekday( + WeekdayMapper.fromDartToIndex.inverse.map(dayIndex), + ), startTime, blocks, room ?? '', @@ -154,7 +163,7 @@ Future> getScheduleFromHtml( ); lecturesList.add(lect); } - day++; + dayIndex++; } semana = semana.expand((i) => [if ((i - 1) < 0) 0 else i - 1]).toList(); } @@ -162,7 +171,7 @@ Future> getScheduleFromHtml( lecturesList ..addAll( - await getOverlappedClasses(session, document, faculty), + await getOverlappedClasses(session, document, faculty, week), ) ..sort((a, b) => a.compare(b)); diff --git a/uni/lib/generated/intl/messages_all.dart b/uni/lib/generated/intl/messages_all.dart index b77f94db2..6b3ebeae5 100644 --- a/uni/lib/generated/intl/messages_all.dart +++ b/uni/lib/generated/intl/messages_all.dart @@ -11,7 +11,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; @@ -21,8 +20,8 @@ import 'messages_pt_PT.dart' as messages_pt_pt; typedef Future LibraryLoader(); Map _deferredLibraries = { - 'en': () => new SynchronousFuture(null), - 'pt_PT': () => new SynchronousFuture(null), + 'en': () => new Future.value(null), + 'pt_PT': () => new Future.value(null), }; MessageLookupByLibrary? _findExact(String localeName) { @@ -37,18 +36,18 @@ MessageLookupByLibrary? _findExact(String localeName) { } /// User programs should call this before using [localeName] for messages. -Future initializeMessages(String localeName) { +Future initializeMessages(String localeName) async { var availableLocale = Intl.verifiedLocale( localeName, (locale) => _deferredLibraries[locale] != null, onFailure: (_) => null); if (availableLocale == null) { - return new SynchronousFuture(false); + return new Future.value(false); } var lib = _deferredLibraries[availableLocale]; - lib == null ? new SynchronousFuture(false) : lib(); + await (lib == null ? new Future.value(false) : lib()); initializeInternalMessageLookup(() => new CompositeMessageLookup()); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new SynchronousFuture(true); + return new Future.value(true); } bool _messagesExistFor(String locale) { diff --git a/uni/lib/generated/intl/messages_en.dart b/uni/lib/generated/intl/messages_en.dart index 112516f05..c7aef77de 100644 --- a/uni/lib/generated/intl/messages_en.dart +++ b/uni/lib/generated/intl/messages_en.dart @@ -7,8 +7,7 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes +// ignore_for_file:unused_import, file_names import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -20,12 +19,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; - static String m0(time) => "last refresh at ${time}"; + static m0(time) => "last refresh at ${time}"; - static String m1(time) => + static m1(time) => "${Intl.plural(time, zero: 'Refreshed ${time} minutes ago', one: 'Refreshed ${time} minute ago', other: 'Refreshed ${time} minutes ago')}"; - static String m2(title) => "${Intl.select(title, { + static m2(title) => "${Intl.select(title, { 'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', @@ -35,14 +34,15 @@ class MessageLookup extends MessageLookupByLibrary { 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', - 'uteis': 'Utils', - 'sobre': 'About', - 'bugs': 'Bugs/Suggestions', + 'percurso_academico': 'Academic Path', + 'transportes': 'Transports', + 'faculdade': 'Faculty', 'other': 'Other', })}"; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { + static _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("About us"), "academic_services": MessageLookupByLibrary.simpleMessage("Academic services"), "account_card_title": @@ -60,6 +60,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Available amount"), "average": MessageLookupByLibrary.simpleMessage("Average: "), "balance": MessageLookupByLibrary.simpleMessage("Balance:"), + "banner_info": MessageLookupByLibrary.simpleMessage( + "We do now collect anonymous usage statistics in order to improve your experience. You can change it in settings."), "bs_description": MessageLookupByLibrary.simpleMessage( "Did you find any bugs in the application?\nDo you have any suggestions for the app?\nTell us so we can improve!"), "bug_description": MessageLookupByLibrary.simpleMessage( @@ -80,6 +82,8 @@ class MessageLookup extends MessageLookupByLibrary { "Check your internet connection"), "class_registration": MessageLookupByLibrary.simpleMessage("Class Registration"), + "collect_usage_stats": + MessageLookupByLibrary.simpleMessage("Collect usage statistics"), "college": MessageLookupByLibrary.simpleMessage("College: "), "college_select": MessageLookupByLibrary.simpleMessage("select your college(s)"), @@ -87,6 +91,8 @@ class MessageLookup extends MessageLookupByLibrary { "configured_buses": MessageLookupByLibrary.simpleMessage("Configured Buses"), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "confirm_logout": MessageLookupByLibrary.simpleMessage( + "Do you really want to log out? Your local data will be deleted and you will have to log in again."), "consent": MessageLookupByLibrary.simpleMessage( "I consent to this information being reviewed by NIAEFEUP and may be deleted at my request."), "contact": MessageLookupByLibrary.simpleMessage("Contact (optional)"), @@ -107,6 +113,8 @@ class MessageLookup extends MessageLookupByLibrary { "D. Beatriz\'s stationery store"), "dona_bia_building": MessageLookupByLibrary.simpleMessage( "Floor -1 of building B (B-142)"), + "download_error": + MessageLookupByLibrary.simpleMessage("Error downloading the file"), "ects": MessageLookupByLibrary.simpleMessage("ECTS performed: "), "edit_off": MessageLookupByLibrary.simpleMessage("Edit"), "edit_on": MessageLookupByLibrary.simpleMessage("Finish editing"), @@ -122,7 +130,8 @@ class MessageLookup extends MessageLookupByLibrary { "fee_date": MessageLookupByLibrary.simpleMessage("Deadline for next fee:"), "fee_notification": - MessageLookupByLibrary.simpleMessage("Notify next deadline:"), + MessageLookupByLibrary.simpleMessage("Fee deadline"), + "files": MessageLookupByLibrary.simpleMessage("Files"), "first_year_registration": MessageLookupByLibrary.simpleMessage( "Year of first registration: "), "floor": MessageLookupByLibrary.simpleMessage("Floor"), @@ -139,6 +148,7 @@ class MessageLookup extends MessageLookupByLibrary { "invalid_credentials": MessageLookupByLibrary.simpleMessage("Invalid credentials"), "keep_login": MessageLookupByLibrary.simpleMessage("Stay signed in"), + "language": MessageLookupByLibrary.simpleMessage("Language"), "last_refresh_time": m0, "last_timestamp": m1, "library_occupation": @@ -157,6 +167,8 @@ class MessageLookup extends MessageLookupByLibrary { "nav_title": m2, "news": MessageLookupByLibrary.simpleMessage("News"), "no": MessageLookupByLibrary.simpleMessage("No"), + "no_app": MessageLookupByLibrary.simpleMessage( + "No app found to open the file"), "no_bus": MessageLookupByLibrary.simpleMessage("Don\'t miss any bus!"), "no_bus_stops": MessageLookupByLibrary.simpleMessage("No configured stops"), @@ -166,20 +178,31 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("No classes to present"), "no_classes_on": MessageLookupByLibrary.simpleMessage("You don\'t have classes on"), + "no_classes_on_weekend": + MessageLookupByLibrary.simpleMessage("You don\'t have classes on"), "no_college": MessageLookupByLibrary.simpleMessage("no college"), "no_course_units": MessageLookupByLibrary.simpleMessage( "No course units in the selected period"), "no_data": MessageLookupByLibrary.simpleMessage( "There is no data to show at this time"), "no_date": MessageLookupByLibrary.simpleMessage("No date"), + "no_events": MessageLookupByLibrary.simpleMessage("No events found"), "no_exams": MessageLookupByLibrary.simpleMessage( "You have no exams scheduled\n"), "no_exams_label": MessageLookupByLibrary.simpleMessage( "Looks like you are on vacation!"), "no_favorite_restaurants": MessageLookupByLibrary.simpleMessage("No favorite restaurants"), + "no_files_found": + MessageLookupByLibrary.simpleMessage("No files found"), "no_info": MessageLookupByLibrary.simpleMessage( "There is no information to display"), + "no_internet": MessageLookupByLibrary.simpleMessage( + "It looks like you\'re offline"), + "no_library_info": MessageLookupByLibrary.simpleMessage( + "No library occupation information available"), + "no_link": + MessageLookupByLibrary.simpleMessage("We couldn\'t open the link"), "no_menu_info": MessageLookupByLibrary.simpleMessage( "There is no information available about meals"), "no_menus": MessageLookupByLibrary.simpleMessage( @@ -188,6 +211,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Unnamed course"), "no_places_info": MessageLookupByLibrary.simpleMessage( "There is no information available about places"), + "no_print_info": MessageLookupByLibrary.simpleMessage( + "No print balance information"), "no_references": MessageLookupByLibrary.simpleMessage( "There are no references to pay"), "no_results": MessageLookupByLibrary.simpleMessage("No match"), @@ -195,14 +220,20 @@ class MessageLookup extends MessageLookupByLibrary { "There are no course units to display"), "no_selected_exams": MessageLookupByLibrary.simpleMessage( "There are no exams to present"), + "notifications": MessageLookupByLibrary.simpleMessage("Notifications"), "occurrence_type": MessageLookupByLibrary.simpleMessage("Type of occurrence"), + "of_month": MessageLookupByLibrary.simpleMessage("of"), + "open_error": + MessageLookupByLibrary.simpleMessage("Error opening the file"), "other_links": MessageLookupByLibrary.simpleMessage("Other links"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "For security reasons, passwords must be changed periodically."), "password": MessageLookupByLibrary.simpleMessage("password"), "pendent_references": MessageLookupByLibrary.simpleMessage("Pending references"), + "permission_denied": + MessageLookupByLibrary.simpleMessage("Permission denied"), "personal_assistance": MessageLookupByLibrary.simpleMessage("Face-to-face assistance"), "press_again": @@ -212,31 +243,41 @@ class MessageLookup extends MessageLookupByLibrary { "problem_id": MessageLookupByLibrary.simpleMessage( "Brief identification of the problem"), "reference_sigarra_help": MessageLookupByLibrary.simpleMessage( - "The generated reference data will appear in Sigarra, checking account.\\nProfile > Checking Account"), + "The generated reference data will appear in Sigarra, checking account.\nProfile > Checking Account"), "reference_success": MessageLookupByLibrary.simpleMessage( "Reference created successfully!"), "remove": MessageLookupByLibrary.simpleMessage("Delete"), "report_error": MessageLookupByLibrary.simpleMessage("Report error"), + "report_error_suggestion": + MessageLookupByLibrary.simpleMessage("Report error/suggestion"), + "restaurant_main_page": MessageLookupByLibrary.simpleMessage( + "Do you want to see your favorite restaurants in the main page?"), "room": MessageLookupByLibrary.simpleMessage("Room"), "school_calendar": MessageLookupByLibrary.simpleMessage("School Calendar"), + "search": MessageLookupByLibrary.simpleMessage("Search"), "semester": MessageLookupByLibrary.simpleMessage("Semester"), "send": MessageLookupByLibrary.simpleMessage("Send"), "sent_error": MessageLookupByLibrary.simpleMessage( "An error occurred in sending"), + "settings": MessageLookupByLibrary.simpleMessage("Settings"), "some_error": MessageLookupByLibrary.simpleMessage("Some error!"), "stcp_stops": MessageLookupByLibrary.simpleMessage("STCP - Upcoming Trips"), "student_number": MessageLookupByLibrary.simpleMessage("student number"), "success": MessageLookupByLibrary.simpleMessage("Sent with success"), + "successful_open": + MessageLookupByLibrary.simpleMessage("File opened successfully"), "tele_assistance": MessageLookupByLibrary.simpleMessage("Telephone assistance"), "tele_personal_assistance": MessageLookupByLibrary.simpleMessage( "Face-to-face and telephone assistance"), "telephone": MessageLookupByLibrary.simpleMessage("Telephone"), "terms": MessageLookupByLibrary.simpleMessage("Terms and Conditions"), + "theme": MessageLookupByLibrary.simpleMessage("Theme"), "title": MessageLookupByLibrary.simpleMessage("Title"), + "uc_info": MessageLookupByLibrary.simpleMessage("Open UC page in app"), "unavailable": MessageLookupByLibrary.simpleMessage("Unavailable"), "valid_email": MessageLookupByLibrary.simpleMessage("Please enter a valid email"), diff --git a/uni/lib/generated/intl/messages_pt_PT.dart b/uni/lib/generated/intl/messages_pt_PT.dart index 68a173ffe..b57f5d6dd 100644 --- a/uni/lib/generated/intl/messages_pt_PT.dart +++ b/uni/lib/generated/intl/messages_pt_PT.dart @@ -7,8 +7,7 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes +// ignore_for_file:unused_import, file_names import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -20,12 +19,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt_PT'; - static String m0(time) => "última atualização às ${time}"; + static m0(time) => "última atualização às ${time}"; - static String m1(time) => + static m1(time) => "${Intl.plural(time, zero: 'Atualizado há ${time} minutos', one: 'Atualizado há ${time} minuto', other: 'Atualizado há ${time} minutos')}"; - static String m2(title) => "${Intl.select(title, { + static m2(title) => "${Intl.select(title, { 'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', @@ -35,14 +34,15 @@ class MessageLookup extends MessageLookupByLibrary { 'restaurantes': 'Restaurantes', 'calendario': 'Calendário', 'biblioteca': 'Biblioteca', - 'uteis': 'Úteis', - 'sobre': 'Sobre', - 'bugs': 'Bugs e Sugestões', + 'percurso_academico': 'Percurso Académico', + 'transportes': 'Transportes', + 'faculdade': 'Faculdade', 'other': 'Outros', })}"; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { + static _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("Sobre nós"), "academic_services": MessageLookupByLibrary.simpleMessage("Serviços académicos"), "account_card_title": @@ -60,6 +60,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Valor disponível"), "average": MessageLookupByLibrary.simpleMessage("Média: "), "balance": MessageLookupByLibrary.simpleMessage("Saldo:"), + "banner_info": MessageLookupByLibrary.simpleMessage( + "Agora recolhemos estatísticas de uso anónimas para melhorar a tua experiência. Podes alterá-lo nas definições."), "bs_description": MessageLookupByLibrary.simpleMessage( "Encontraste algum bug na aplicação?\nTens alguma sugestão para a app?\nConta-nos para que possamos melhorar!"), "bug_description": MessageLookupByLibrary.simpleMessage( @@ -72,7 +74,7 @@ class MessageLookup extends MessageLookupByLibrary { "Configura aqui os teus autocarros"), "buses_text": MessageLookupByLibrary.simpleMessage( "Os autocarros favoritos serão apresentados no widget \'Autocarros\' dos favoritos. Os restantes serão apresentados apenas na página."), - "cancel": MessageLookupByLibrary.simpleMessage("Cancelar\n"), + "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), "change": MessageLookupByLibrary.simpleMessage("Alterar"), "change_prompt": MessageLookupByLibrary.simpleMessage( "Deseja alterar a palavra-passe?"), @@ -80,6 +82,8 @@ class MessageLookup extends MessageLookupByLibrary { "Verifica a tua ligação à internet"), "class_registration": MessageLookupByLibrary.simpleMessage("Inscrição de Turmas"), + "collect_usage_stats": MessageLookupByLibrary.simpleMessage( + "Partilhar estatísticas de uso"), "college": MessageLookupByLibrary.simpleMessage("Faculdade: "), "college_select": MessageLookupByLibrary.simpleMessage( "seleciona a(s) tua(s) faculdade(s)"), @@ -87,6 +91,8 @@ class MessageLookup extends MessageLookupByLibrary { "configured_buses": MessageLookupByLibrary.simpleMessage("Autocarros Configurados"), "confirm": MessageLookupByLibrary.simpleMessage("Confirmar"), + "confirm_logout": MessageLookupByLibrary.simpleMessage( + "Tens a certeza de que queres terminar sessão? Os teus dados locais serão apagados e terás de iniciar sessão novamente."), "consent": MessageLookupByLibrary.simpleMessage( "Consinto que esta informação seja revista pelo NIAEFEUP, podendo ser eliminada a meu pedido."), "contact": MessageLookupByLibrary.simpleMessage("Contacto (opcional)"), @@ -106,8 +112,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Papelaria D. Beatriz"), "dona_bia_building": MessageLookupByLibrary.simpleMessage( "Piso -1 do edifício B (B-142)"), + "download_error": MessageLookupByLibrary.simpleMessage( + "Erro ao descarregar o ficheiro"), "ects": MessageLookupByLibrary.simpleMessage("ECTS realizados: "), - "edit_off": MessageLookupByLibrary.simpleMessage("Editar\n"), + "edit_off": MessageLookupByLibrary.simpleMessage("Editar"), "edit_on": MessageLookupByLibrary.simpleMessage("Concluir edição"), "empty_text": MessageLookupByLibrary.simpleMessage( "Por favor preenche este campo"), @@ -120,8 +128,9 @@ class MessageLookup extends MessageLookupByLibrary { "failed_login": MessageLookupByLibrary.simpleMessage("O login falhou"), "fee_date": MessageLookupByLibrary.simpleMessage( "Data limite próxima prestação:"), - "fee_notification": MessageLookupByLibrary.simpleMessage( - "Notificar próxima data limite:"), + "fee_notification": + MessageLookupByLibrary.simpleMessage("Data limite de propina"), + "files": MessageLookupByLibrary.simpleMessage("Ficheiros"), "first_year_registration": MessageLookupByLibrary.simpleMessage("Ano da primeira inscrição: "), "floor": MessageLookupByLibrary.simpleMessage("Piso"), @@ -139,12 +148,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Credenciais inválidas"), "keep_login": MessageLookupByLibrary.simpleMessage("Manter sessão iniciada"), + "language": MessageLookupByLibrary.simpleMessage("Idioma"), "last_refresh_time": m0, "last_timestamp": m1, "library_occupation": MessageLookupByLibrary.simpleMessage("Ocupação da Biblioteca"), "load_error": MessageLookupByLibrary.simpleMessage( - "Aconteceu um erro ao carregar os dados"), + "Erro ao carregar a informação"), "loading_terms": MessageLookupByLibrary.simpleMessage( "Carregando os Termos e Condições..."), "login": MessageLookupByLibrary.simpleMessage("Entrar"), @@ -157,6 +167,8 @@ class MessageLookup extends MessageLookupByLibrary { "nav_title": m2, "news": MessageLookupByLibrary.simpleMessage("Notícias"), "no": MessageLookupByLibrary.simpleMessage("Não"), + "no_app": MessageLookupByLibrary.simpleMessage( + "Nenhuma aplicação encontrada para abrir o ficheiro"), "no_bus": MessageLookupByLibrary.simpleMessage( "Não percas nenhum autocarro!"), "no_bus_stops": MessageLookupByLibrary.simpleMessage( @@ -167,20 +179,32 @@ class MessageLookup extends MessageLookupByLibrary { "Não existem aulas para apresentar"), "no_classes_on": MessageLookupByLibrary.simpleMessage("Não possui aulas à"), + "no_classes_on_weekend": + MessageLookupByLibrary.simpleMessage("Não possui aulas ao"), "no_college": MessageLookupByLibrary.simpleMessage("sem faculdade"), "no_course_units": MessageLookupByLibrary.simpleMessage( "Sem cadeiras no período selecionado"), "no_data": MessageLookupByLibrary.simpleMessage( "Não há dados a mostrar neste momento"), "no_date": MessageLookupByLibrary.simpleMessage("Sem data"), + "no_events": + MessageLookupByLibrary.simpleMessage("Nenhum evento encontrado"), "no_exams": MessageLookupByLibrary.simpleMessage("Não possui exames marcados"), "no_exams_label": MessageLookupByLibrary.simpleMessage("Parece que estás de férias!"), "no_favorite_restaurants": MessageLookupByLibrary.simpleMessage("Sem restaurantes favoritos"), + "no_files_found": + MessageLookupByLibrary.simpleMessage("Nenhum ficheiro encontrado"), "no_info": MessageLookupByLibrary.simpleMessage( "Não existem informações para apresentar"), + "no_internet": + MessageLookupByLibrary.simpleMessage("Parece que estás offline"), + "no_library_info": + MessageLookupByLibrary.simpleMessage("Sem informação de ocupação"), + "no_link": MessageLookupByLibrary.simpleMessage( + "Não conseguimos abrir o link"), "no_menu_info": MessageLookupByLibrary.simpleMessage( "Não há informação disponível sobre refeições"), "no_menus": MessageLookupByLibrary.simpleMessage( @@ -189,6 +213,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Curso sem nome"), "no_places_info": MessageLookupByLibrary.simpleMessage( "Não há informação disponível sobre locais"), + "no_print_info": + MessageLookupByLibrary.simpleMessage("Sem informação de saldo"), "no_references": MessageLookupByLibrary.simpleMessage( "Não existem referências a pagar"), "no_results": MessageLookupByLibrary.simpleMessage("Sem resultados"), @@ -196,14 +222,20 @@ class MessageLookup extends MessageLookupByLibrary { "Não existem cadeiras para apresentar"), "no_selected_exams": MessageLookupByLibrary.simpleMessage( "Não existem exames para apresentar"), + "notifications": MessageLookupByLibrary.simpleMessage("Notificações"), "occurrence_type": MessageLookupByLibrary.simpleMessage("Tipo de ocorrência"), + "of_month": MessageLookupByLibrary.simpleMessage("de"), + "open_error": + MessageLookupByLibrary.simpleMessage("Erro ao abrir o ficheiro"), "other_links": MessageLookupByLibrary.simpleMessage("Outros links"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente."), "password": MessageLookupByLibrary.simpleMessage("palavra-passe"), "pendent_references": MessageLookupByLibrary.simpleMessage("Referências pendentes"), + "permission_denied": + MessageLookupByLibrary.simpleMessage("Sem permissão"), "personal_assistance": MessageLookupByLibrary.simpleMessage("Atendimento presencial"), "press_again": MessageLookupByLibrary.simpleMessage( @@ -213,31 +245,42 @@ class MessageLookup extends MessageLookupByLibrary { "problem_id": MessageLookupByLibrary.simpleMessage( "Breve identificação do problema"), "reference_sigarra_help": MessageLookupByLibrary.simpleMessage( - "Os dados da referência gerada aparecerão no Sigarra, conta corrente.\\nPerfil > Conta Corrente"), + "Os dados da referência gerada aparecerão no Sigarra, conta corrente. Perfil > Conta Corrente"), "reference_success": MessageLookupByLibrary.simpleMessage( "Referência criada com sucesso!"), "remove": MessageLookupByLibrary.simpleMessage("Remover"), "report_error": MessageLookupByLibrary.simpleMessage("Reportar erro"), + "report_error_suggestion": + MessageLookupByLibrary.simpleMessage("Reportar erro/sugestão"), + "restaurant_main_page": MessageLookupByLibrary.simpleMessage( + "Queres ver os teus restaurantes favoritos na página principal?"), "room": MessageLookupByLibrary.simpleMessage("Sala"), "school_calendar": MessageLookupByLibrary.simpleMessage("Calendário Escolar"), + "search": MessageLookupByLibrary.simpleMessage("Pesquisar"), "semester": MessageLookupByLibrary.simpleMessage("Semestre"), "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sent_error": MessageLookupByLibrary.simpleMessage("Ocorreu um erro no envio"), + "settings": MessageLookupByLibrary.simpleMessage("Definições"), "some_error": MessageLookupByLibrary.simpleMessage("Algum erro!"), "stcp_stops": MessageLookupByLibrary.simpleMessage("STCP - Próximas Viagens"), "student_number": MessageLookupByLibrary.simpleMessage("número de estudante"), "success": MessageLookupByLibrary.simpleMessage("Enviado com sucesso"), + "successful_open": + MessageLookupByLibrary.simpleMessage("Ficheiro aberto com sucesso"), "tele_assistance": MessageLookupByLibrary.simpleMessage("Atendimento telefónico"), "tele_personal_assistance": MessageLookupByLibrary.simpleMessage( "Atendimento presencial e telefónico"), "telephone": MessageLookupByLibrary.simpleMessage("Telefone"), "terms": MessageLookupByLibrary.simpleMessage("Termos e Condições"), + "theme": MessageLookupByLibrary.simpleMessage("Tema"), "title": MessageLookupByLibrary.simpleMessage("Título"), + "uc_info": + MessageLookupByLibrary.simpleMessage("Abrir página da UC na app"), "unavailable": MessageLookupByLibrary.simpleMessage("Indisponível"), "valid_email": MessageLookupByLibrary.simpleMessage( "Por favor insere um email válido"), diff --git a/uni/lib/generated/l10n.dart b/uni/lib/generated/l10n.dart index 936259b3e..16ea5a058 100644 --- a/uni/lib/generated/l10n.dart +++ b/uni/lib/generated/l10n.dart @@ -80,6 +80,56 @@ class S { ); } + /// `About us` + String get about { + return Intl.message( + 'About us', + name: 'about', + desc: '', + args: [], + ); + } + + /// `Report error/suggestion` + String get report_error_suggestion { + return Intl.message( + 'Report error/suggestion', + name: 'report_error_suggestion', + desc: '', + args: [], + ); + } + + /// `Language` + String get language { + return Intl.message( + 'Language', + name: 'language', + desc: '', + args: [], + ); + } + + /// `Theme` + String get theme { + return Intl.message( + 'Theme', + name: 'theme', + desc: '', + args: [], + ); + } + + /// `Notifications` + String get notifications { + return Intl.message( + 'Notifications', + name: 'notifications', + desc: '', + args: [], + ); + } + /// `Academic services` String get academic_services { return Intl.message( @@ -180,6 +230,16 @@ class S { ); } + /// `We do now collect anonymous usage statistics in order to improve your experience. You can change it in settings.` + String get banner_info { + return Intl.message( + 'We do now collect anonymous usage statistics in order to improve your experience. You can change it in settings.', + name: 'banner_info', + desc: '', + args: [], + ); + } + /// `Balance:` String get balance { return Intl.message( @@ -560,10 +620,10 @@ class S { ); } - /// `Notify next deadline:` + /// `Fee deadline` String get fee_notification { return Intl.message( - 'Notify next deadline:', + 'Fee deadline', name: 'fee_notification', desc: '', args: [], @@ -703,11 +763,11 @@ class S { ); } - /// `Error loading the information` - String get load_error { + /// `Error downloading the file` + String get download_error { return Intl.message( - 'Error loading the information', - name: 'load_error', + 'Error downloading the file', + name: 'download_error', desc: '', args: [], ); @@ -733,6 +793,16 @@ class S { ); } + /// `Settings` + String get settings { + return Intl.message( + 'Settings', + name: 'settings', + desc: '', + args: [], + ); + } + /// `Log out` String get logout { return Intl.message( @@ -773,7 +843,7 @@ class S { ); } - /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} uteis{Utils} sobre{About} bugs{Bugs/Suggestions} other{Other}}` + /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} transportes{Transports} faculdade{Faculty} other{Other}}` String nav_title(Object title) { return Intl.select( title, @@ -787,9 +857,9 @@ class S { 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', - 'uteis': 'Utils', - 'sobre': 'About', - 'bugs': 'Bugs/Suggestions', + 'percurso_academico': 'Academic Path', + 'transportes': 'Transports', + 'faculdade': 'Faculty', 'other': 'Other', }, name: 'nav_title', @@ -858,6 +928,16 @@ class S { ); } + /// `You don't have classes on` + String get no_classes_on_weekend { + return Intl.message( + 'You don\'t have classes on', + name: 'no_classes_on_weekend', + desc: '', + args: [], + ); + } + /// `no college` String get no_college { return Intl.message( @@ -898,6 +978,16 @@ class S { ); } + /// `No events found` + String get no_events { + return Intl.message( + 'No events found', + name: 'no_events', + desc: '', + args: [], + ); + } + /// `You have no exams scheduled\n` String get no_exams { return Intl.message( @@ -1018,6 +1108,26 @@ class S { ); } + /// `No print balance information` + String get no_print_info { + return Intl.message( + 'No print balance information', + name: 'no_print_info', + desc: '', + args: [], + ); + } + + /// `No library occupation information available` + String get no_library_info { + return Intl.message( + 'No library occupation information available', + name: 'no_library_info', + desc: '', + args: [], + ); + } + /// `Type of occurrence` String get occurrence_type { return Intl.message( @@ -1028,6 +1138,46 @@ class S { ); } + /// `of` + String get of_month { + return Intl.message( + 'of', + name: 'of_month', + desc: '', + args: [], + ); + } + + /// `We couldn't open the link` + String get no_link { + return Intl.message( + 'We couldn\'t open the link', + name: 'no_link', + desc: '', + args: [], + ); + } + + /// `It looks like you're offline` + String get no_internet { + return Intl.message( + 'It looks like you\'re offline', + name: 'no_internet', + desc: '', + args: [], + ); + } + + /// `No files found` + String get no_files_found { + return Intl.message( + 'No files found', + name: 'no_files_found', + desc: '', + args: [], + ); + } + /// `Other links` String get other_links { return Intl.message( @@ -1098,6 +1248,56 @@ class S { ); } + /// `File opened successfully` + String get successful_open { + return Intl.message( + 'File opened successfully', + name: 'successful_open', + desc: '', + args: [], + ); + } + + /// `Permission denied` + String get permission_denied { + return Intl.message( + 'Permission denied', + name: 'permission_denied', + desc: '', + args: [], + ); + } + + /// `Error opening the file` + String get open_error { + return Intl.message( + 'Error opening the file', + name: 'open_error', + desc: '', + args: [], + ); + } + + /// `No app found to open the file` + String get no_app { + return Intl.message( + 'No app found to open the file', + name: 'no_app', + desc: '', + args: [], + ); + } + + /// `Error loading the information` + String get load_error { + return Intl.message( + 'Error loading the information', + name: 'load_error', + desc: '', + args: [], + ); + } + /// `Prints` String get prints { return Intl.message( @@ -1121,7 +1321,7 @@ class S { /// `The generated reference data will appear in Sigarra, checking account.\nProfile > Checking Account` String get reference_sigarra_help { return Intl.message( - 'The generated reference data will appear in Sigarra, checking account.\\nProfile > Checking Account', + 'The generated reference data will appear in Sigarra, checking account.\nProfile > Checking Account', name: 'reference_sigarra_help', desc: '', args: [], @@ -1158,6 +1358,16 @@ class S { ); } + /// `Do you want to see your favorite restaurants in the main page?` + String get restaurant_main_page { + return Intl.message( + 'Do you want to see your favorite restaurants in the main page?', + name: 'restaurant_main_page', + desc: '', + args: [], + ); + } + /// `Room` String get room { return Intl.message( @@ -1168,6 +1378,16 @@ class S { ); } + /// `Files` + String get files { + return Intl.message( + 'Files', + name: 'files', + desc: '', + args: [], + ); + } + /// `School Calendar` String get school_calendar { return Intl.message( @@ -1298,6 +1518,16 @@ class S { ); } + /// `Open UC page` + String get uc_info { + return Intl.message( + 'Open UC page', + name: 'uc_info', + desc: '', + args: [], + ); + } + /// `Unavailable` String get unavailable { return Intl.message( @@ -1337,6 +1567,36 @@ class S { args: [], ); } + + /// `Search` + String get search { + return Intl.message( + 'Search', + name: 'search', + desc: '', + args: [], + ); + } + + /// `Do you really want to log out? Your local data will be deleted and you will have to log in again.` + String get confirm_logout { + return Intl.message( + 'Do you really want to log out? Your local data will be deleted and you will have to log in again.', + name: 'confirm_logout', + desc: '', + args: [], + ); + } + + /// `Collect usage statistics` + String get collect_usage_stats { + return Intl.message( + 'Collect usage statistics', + name: 'collect_usage_stats', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/uni/lib/l10n/intl_en.arb b/uni/lib/l10n/intl_en.arb index b1d6be897..408136d18 100644 --- a/uni/lib/l10n/intl_en.arb +++ b/uni/lib/l10n/intl_en.arb @@ -6,6 +6,16 @@ "@no": {}, "yes": "Yes", "@yes": {}, + "about": "About us", + "@about": {}, + "report_error_suggestion": "Report error/suggestion", + "@report_error_suggestion": {}, + "language": "Language", + "@language": {}, + "theme": "Theme", + "@theme": {}, + "notifications": "Notifications", + "@notifications": {}, "academic_services": "Academic services", "@academic_services": {}, "account_card_title": "Checking account", @@ -26,6 +36,8 @@ "@available_amount": {}, "average": "Average: ", "@average": {}, + "banner_info": "We do now collect anonymous usage statistics in order to improve your experience. You can change it in settings.", + "@banner_info": {}, "balance": "Balance:", "@balance": {}, "bs_description": "Did you find any bugs in the application?\nDo you have any suggestions for the app?\nTell us so we can improve!", @@ -102,7 +114,7 @@ "@failed_login": {}, "fee_date": "Deadline for next fee:", "@fee_date": {}, - "fee_notification": "Notify next deadline:", + "fee_notification": "Fee deadline", "@fee_notification": {}, "first_year_registration": "Year of first registration: ", "@first_year_registration": {}, @@ -138,12 +150,14 @@ }, "library_occupation": "Library Occupation", "@library_occupation": {}, - "load_error": "Error loading the information", - "@load_error": {}, + "download_error": "Error downloading the file", + "@download_error": {}, "loading_terms": "Loading Terms and Conditions...", "@loading_terms": {}, "login": "Login", "@login": {}, + "settings": "Settings", + "@settings": {}, "logout": "Log out", "@logout": {}, "menus": "Menus", @@ -152,7 +166,7 @@ "@min_value_reference": {}, "multimedia_center": "Multimedia center", "@multimedia_center": {}, - "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} uteis{Utils} sobre{About} bugs{Bugs/Suggestions} other{Other}}", + "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} transportes{Transports} faculdade{Faculty} other{Other}}", "@nav_title": {}, "news": "News", "@news": {}, @@ -166,6 +180,8 @@ "@no_classes": {}, "no_classes_on": "You don't have classes on", "@no_classes_on": {}, + "no_classes_on_weekend": "You don't have classes on", + "@no_classes_on_weekend": {}, "no_college": "no college", "@no_college": {}, "no_course_units": "No course units in the selected period", @@ -174,6 +190,8 @@ "@no_data": {}, "no_date": "No date", "@no_date": {}, + "no_events": "No events found", + "@no_events": {}, "no_exams": "You have no exams scheduled\n", "@no_exams": {}, "no_exams_label": "Looks like you are on vacation!", @@ -198,8 +216,20 @@ "@no_selected_courses": {}, "no_selected_exams": "There are no exams to present", "@no_selected_exams": {}, + "no_print_info": "No print balance information", + "@no_print_info": {}, + "no_library_info": "No library occupation information available", + "@no_library_info": {}, "occurrence_type": "Type of occurrence", "@occurrence_type": {}, + "of_month": "of", + "@of_month": {}, + "no_link": "We couldn't open the link", + "@no_link": {}, + "no_internet": "It looks like you're offline", + "@no_internet": {}, + "no_files_found": "No files found", + "@no_files_found": {}, "other_links": "Other links", "@other_links": {}, "pass_change_request": "For security reasons, passwords must be changed periodically.", @@ -214,11 +244,21 @@ "@press_again": {}, "print": "Print", "@print": {}, + "successful_open": "File opened successfully", + "@successful_open": {}, + "permission_denied": "Permission denied", + "@permission_denied": {}, + "open_error": "Error opening the file", + "@open_error": {}, + "no_app": "No app found to open the file", + "@no_app": {}, + "load_error": "Error loading the information", + "@load_error": {}, "prints": "Prints", "@prints": {}, "problem_id": "Brief identification of the problem", "@problem_id": {}, - "reference_sigarra_help": "The generated reference data will appear in Sigarra, checking account.\\nProfile > Checking Account", + "reference_sigarra_help": "The generated reference data will appear in Sigarra, checking account.\nProfile > Checking Account", "@reference_sigarra_help": {}, "reference_success": "Reference created successfully!", "@reference_success": {}, @@ -226,8 +266,12 @@ "@remove": {}, "report_error": "Report error", "@report_error": {}, + "restaurant_main_page": "Do you want to see your favorite restaurants in the main page?", + "@restaurant_main_page": {}, "room": "Room", "@room": {}, + "files": "Files", + "@files": {}, "school_calendar": "School Calendar", "@school_calendar": {}, "semester": "Semester", @@ -254,6 +298,8 @@ "@terms": {}, "title": "Title", "@title": {}, + "uc_info": "Open UC page", + "@uc_info": {}, "unavailable": "Unavailable", "@unavailable": {}, "valid_email": "Please enter a valid email", @@ -261,5 +307,11 @@ "widget_prompt": "Choose a widget to add to your personal area:", "@widget_prompt": {}, "year": "Year", - "@year": {} + "@year": {}, + "search": "Search", + "@search": {}, + "confirm_logout": "Do you really want to log out? Your local data will be deleted and you will have to log in again.", + "@confirm_logout": {}, + "collect_usage_stats": "Collect usage statistics", + "@usage_stats": {} } \ No newline at end of file diff --git a/uni/lib/l10n/intl_pt_PT.arb b/uni/lib/l10n/intl_pt_PT.arb index 4315ac0af..012c4b201 100644 --- a/uni/lib/l10n/intl_pt_PT.arb +++ b/uni/lib/l10n/intl_pt_PT.arb @@ -1,11 +1,23 @@ { "@@locale": "pt_PT", + "settings": "Definições", + "@settings": {}, "exit_confirm": "Tem a certeza de que pretende sair?", "@exit_confirm": {}, + "about": "Sobre nós", + "@about": {}, "no": "Não", "@no": {}, "yes": "Sim", "@yes": {}, + "report_error_suggestion": "Reportar erro/sugestão", + "@report_error_suggestion": {}, + "language": "Idioma", + "@language": {}, + "theme": "Tema", + "@theme": {}, + "notifications": "Notificações", + "@notifications": {}, "academic_services": "Serviços académicos", "@academic_services": {}, "account_card_title": "Conta Corrente", @@ -26,6 +38,8 @@ "@available_amount": {}, "average": "Média: ", "@average": {}, + "banner_info": "Agora recolhemos estatísticas de uso anónimas para melhorar a tua experiência. Podes alterá-lo nas definições.", + "@banner_info": {}, "balance": "Saldo:", "@balance": {}, "bs_description": "Encontraste algum bug na aplicação?\nTens alguma sugestão para a app?\nConta-nos para que possamos melhorar!", @@ -40,7 +54,7 @@ "@buses_text": {}, "bus_information": "Seleciona os autocarros dos quais queres informação:", "@bus_information": {}, - "cancel": "Cancelar\n", + "cancel": "Cancelar", "@cancel": {}, "change": "Alterar", "@change": {}, @@ -88,7 +102,7 @@ "@dona_bia_building": {}, "ects": "ECTS realizados: ", "@ects": {}, - "edit_off": "Editar\n", + "edit_off": "Editar", "@edit_off": {}, "edit_on": "Concluir edição", "@edit_on": {}, @@ -102,7 +116,7 @@ "@failed_login": {}, "fee_date": "Data limite próxima prestação:", "@fee_date": {}, - "fee_notification": "Notificar próxima data limite:", + "fee_notification": "Data limite de propina", "@fee_notification": {}, "first_year_registration": "Ano da primeira inscrição: ", "@first_year_registration": {}, @@ -136,10 +150,20 @@ "time": {} } }, + "load_error": "Erro ao carregar a informação", + "@load_error": {}, "library_occupation": "Ocupação da Biblioteca", "@library_occupation": {}, - "load_error": "Aconteceu um erro ao carregar os dados", - "@load_error": {}, + "download_error": "Erro ao descarregar o ficheiro", + "@download_error": {}, + "successful_open": "Ficheiro aberto com sucesso", + "@successful_open": {}, + "permission_denied": "Sem permissão", + "@permission_denied": {}, + "open_error": "Erro ao abrir o ficheiro", + "@open_error": {}, + "no_app": "Nenhuma aplicação encontrada para abrir o ficheiro", + "@no_app": {}, "loading_terms": "Carregando os Termos e Condições...", "@loading_terms": {}, "login": "Entrar", @@ -152,7 +176,7 @@ "@min_value_reference": {}, "multimedia_center": "Centro de multimédia", "@multimedia_center": {}, - "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} uteis{Úteis} sobre{Sobre} bugs{Bugs e Sugestões} other{Outros}}", + "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} percurso_academico{Percurso Académico} transportes{Transportes} faculdade{Faculdade} other{Outros}}", "@nav_title": {}, "news": "Notícias", "@news": {}, @@ -166,6 +190,8 @@ "@no_classes": {}, "no_classes_on": "Não possui aulas à", "@no_classes_on": {}, + "no_classes_on_weekend": "Não possui aulas ao", + "@no_classes_on_weekend": {}, "no_college": "sem faculdade", "@no_college": {}, "no_course_units": "Sem cadeiras no período selecionado", @@ -176,6 +202,8 @@ "@no_date": {}, "no_exams": "Não possui exames marcados", "@no_exams": {}, + "no_events": "Nenhum evento encontrado", + "@no_events": {}, "no_exams_label": "Parece que estás de férias!", "@no_exams_label": {}, "no_favorite_restaurants": "Sem restaurantes favoritos", @@ -198,8 +226,20 @@ "@no_selected_courses": {}, "no_selected_exams": "Não existem exames para apresentar", "@no_selected_exams": {}, + "no_internet": "Parece que estás offline", + "@no_internet": {}, + "no_print_info": "Sem informação de saldo", + "@no_print_info": {}, + "no_library_info": "Sem informação de ocupação", + "@no_library_info": {}, "occurrence_type": "Tipo de ocorrência", "@occurrence_type": {}, + "of_month": "de", + "@of_month": {}, + "no_link": "Não conseguimos abrir o link", + "@no_link": {}, + "no_files_found": "Nenhum ficheiro encontrado", + "@no_files_found": {}, "other_links": "Outros links", "@other_links": {}, "pass_change_request": "Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.", @@ -218,7 +258,7 @@ "@prints": {}, "problem_id": "Breve identificação do problema", "@problem_id": {}, - "reference_sigarra_help": "Os dados da referência gerada aparecerão no Sigarra, conta corrente.\\nPerfil > Conta Corrente", + "reference_sigarra_help": "Os dados da referência gerada aparecerão no Sigarra, conta corrente. Perfil > Conta Corrente", "@reference_sigarra_help": {}, "reference_success": "Referência criada com sucesso!", "@reference_success": {}, @@ -226,8 +266,12 @@ "@remove": {}, "report_error": "Reportar erro", "@report_error": {}, + "restaurant_main_page": "Queres ver os teus restaurantes favoritos na página principal?", + "@restaurant_main_page": {}, "room": "Sala", "@room": {}, + "files": "Ficheiros", + "@files": {}, "school_calendar": "Calendário Escolar", "@school_calendar": {}, "semester": "Semestre", @@ -254,6 +298,8 @@ "@terms": {}, "title": "Título", "@title": {}, + "uc_info": "Abrir página da UC", + "@uc_info": {}, "unavailable": "Indisponível", "@unavailable": {}, "valid_email": "Por favor insere um email válido", @@ -261,5 +307,11 @@ "widget_prompt": "Escolhe um widget para adicionares à tua área pessoal:", "@widget_prompt": {}, "year": "Ano", - "@year": {} + "@year": {}, + "search": "Pesquisar", + "@search": {}, + "confirm_logout": "Tens a certeza de que queres terminar sessão? Os teus dados locais serão apagados e terás de iniciar sessão novamente.", + "@confirm_logout": {}, + "collect_usage_stats": "Partilhar estatísticas de uso", + "@collect_usage_stats": {} } \ No newline at end of file diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 01fac7edd..854cf9755 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -6,62 +6,71 @@ import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:logger/logger.dart'; +import 'package:plausible_analytics/navigator_observer.dart'; +import 'package:plausible_analytics/plausible_analytics.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:uni/controller/background_workers/background_callback.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/cleanup.dart'; +import 'package:uni/controller/fetchers/terms_and_conditions_fetcher.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.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/plausible/plausible_provider.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/about/about.dart'; -import 'package:uni/view/bug_report/bug_report.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/academic_path/academic_path.dart'; import 'package:uni/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart'; import 'package:uni/view/calendar/calendar.dart'; 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/faculty/faculty.dart'; import 'package:uni/view/home/home.dart'; import 'package:uni/view/library/library.dart'; import 'package:uni/view/locale_notifier.dart'; import 'package:uni/view/locations/locations.dart'; import 'package:uni/view/login/login.dart'; -import 'package:uni/view/navigation_service.dart'; +import 'package:uni/view/profile/profile.dart'; import 'package:uni/view/restaurant/restaurant_page_view.dart'; import 'package:uni/view/schedule/schedule.dart'; +import 'package:uni/view/settings/settings.dart'; import 'package:uni/view/theme.dart'; import 'package:uni/view/theme_notifier.dart'; -import 'package:uni/view/useful_info/useful_info.dart'; +import 'package:uni/view/transports/transports.dart'; import 'package:workmanager/workmanager.dart'; SentryEvent? beforeSend(SentryEvent event) { return event.level == SentryLevel.info ? event : null; } -Future firstRoute() async { - final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); +Future firstRoute() async { + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); if (userPersistentInfo != null) { - return '/${DrawerItem.navPersonalArea.title}'; + return const HomePageView(); } await acceptTermsAndConditions(); - return '/${DrawerItem.navLogIn.title}'; + return const LoginPageView(); } Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + PreferencesController.prefs = await SharedPreferences.getInstance(); + final stateProviders = StateProviders( LectureProvider(), ExamProvider(), @@ -73,11 +82,10 @@ Future main() async { CalendarProvider(), LibraryOccupationProvider(), FacultyLocationsProvider(), - HomePageProvider(), ReferenceProvider(), ); - WidgetsFlutterBinding.ensureInitialized(); + unawaited(cleanupCachedFiles()); // Initialize WorkManager for background tasks await Workmanager().initialize( @@ -97,8 +105,20 @@ Future main() async { ); }); - final savedTheme = await AppSharedPreferences.getThemeMode(); - final savedLocale = await AppSharedPreferences.getLocale(); + final plausibleUrl = dotenv.env['PLAUSIBLE_URL']; + final plausibleDomain = dotenv.env['PLAUSIBLE_DOMAIN']; + + final plausible = plausibleUrl != null && plausibleDomain != null + ? Plausible(plausibleUrl, plausibleDomain) + : null; + + if (plausible == null) { + Logger().w('Plausible is not enabled'); + } + + final savedTheme = PreferencesController.getThemeMode(); + final savedLocale = PreferencesController.getLocale(); + final route = await firstRoute(); await SentryFlutter.init( @@ -108,52 +128,52 @@ Future main() async { }, appRunner: () { runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (context) => stateProviders.lectureProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.examProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.busStopProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.restaurantProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.profileProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.courseUnitsInfoProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.sessionProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.calendarProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.libraryOccupationProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.facultyLocationsProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.homePageProvider, - ), - ChangeNotifierProvider( - create: (context) => stateProviders.referenceProvider, - ), - ChangeNotifierProvider( - create: (_) => LocaleNotifier(savedLocale), - ), - ChangeNotifierProvider( - create: (_) => ThemeNotifier(savedTheme), - ), - ], - child: Application(route), + PlausibleProvider( + plausible: plausible, + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => stateProviders.lectureProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.examProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.busStopProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.restaurantProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.profileProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.courseUnitsInfoProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.sessionProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.calendarProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.libraryOccupationProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.facultyLocationsProvider, + ), + ChangeNotifierProvider( + create: (context) => stateProviders.referenceProvider, + ), + ChangeNotifierProvider( + create: (_) => LocaleNotifier(savedLocale), + ), + ChangeNotifierProvider( + create: (_) => ThemeNotifier(savedTheme), + ), + ], + child: Application(route), + ), ), ); }, @@ -164,9 +184,9 @@ Future main() async { /// This class is necessary to track the app's state for /// the current execution. class Application extends StatefulWidget { - const Application(this.initialRoute, {super.key}); + const Application(this.initialWidget, {super.key}); - final String initialRoute; + final Widget initialWidget; static GlobalKey navigatorKey = GlobalKey(); @@ -176,6 +196,18 @@ class Application extends StatefulWidget { /// Manages the app depending on its current state class ApplicationState extends State { + final navigatorObservers = []; + + @override + void initState() { + super.initState(); + + final plausible = context.read(); + if (plausible != null) { + navigatorObservers.add(PlausibleNavigatorObserver(plausible)); + } + } + @override Widget build(BuildContext context) { SystemChrome.setPreferredOrientations([ @@ -196,73 +228,79 @@ class ApplicationState extends State { GlobalCupertinoLocalizations.delegate, ], supportedLocales: S.delegate.supportedLocales, - initialRoute: widget.initialRoute, + initialRoute: '/${NavigationItem.navPersonalArea.route}', + home: widget.initialWidget, + navigatorObservers: navigatorObservers, onGenerateRoute: (RouteSettings settings) { final transitions = { - '/${DrawerItem.navPersonalArea.title}': + '/${NavigationItem.navPersonalArea.route}': PageTransition.makePageTransition( page: const HomePageView(), settings: settings, ), - '/${DrawerItem.navSchedule.title}': + '/${NavigationItem.navSchedule.route}': PageTransition.makePageTransition( - page: const SchedulePage(), + page: SchedulePage(), settings: settings, ), - '/${DrawerItem.navExams.title}': PageTransition.makePageTransition( + '/${NavigationItem.navExams.route}': + PageTransition.makePageTransition( page: const ExamsPageView(), settings: settings, ), - '/${DrawerItem.navStops.title}': PageTransition.makePageTransition( + '/${NavigationItem.navStops.route}': + PageTransition.makePageTransition( page: const BusStopNextArrivalsPage(), settings: settings, ), - '/${DrawerItem.navCourseUnits.title}': + '/${NavigationItem.navCourseUnits.route}': PageTransition.makePageTransition( page: const CourseUnitsPageView(), settings: settings, ), - '/${DrawerItem.navLocations.title}': + '/${NavigationItem.navLocations.route}': PageTransition.makePageTransition( page: const LocationsPage(), settings: settings, ), - '/${DrawerItem.navRestaurants.title}': + '/${NavigationItem.navRestaurants.route}': PageTransition.makePageTransition( page: const RestaurantPageView(), settings: settings, ), - '/${DrawerItem.navCalendar.title}': + '/${NavigationItem.navCalendar.route}': PageTransition.makePageTransition( page: const CalendarPageView(), settings: settings, ), - '/${DrawerItem.navLibrary.title}': + '/${NavigationItem.navLibrary.route}': PageTransition.makePageTransition( - page: const LibraryPageView(), + page: const LibraryPage(), settings: settings, ), - '/${DrawerItem.navUsefulInfo.title}': + '/${NavigationItem.navFaculty.route}': PageTransition.makePageTransition( - page: const UsefulInfoPageView(), + page: const FacultyPageView(), settings: settings, ), - '/${DrawerItem.navAbout.title}': PageTransition.makePageTransition( - page: const AboutPageView(), + '/${NavigationItem.navAcademicPath.route}': + PageTransition.makePageTransition( + page: const AcademicPathPageView(), settings: settings, ), - '/${DrawerItem.navBugReport.title}': + '/${NavigationItem.navTransports.route}': PageTransition.makePageTransition( - page: const BugReportPageView(), + page: const TransportsPageView(), settings: settings, - maintainState: false, ), - '/${DrawerItem.navLogIn.title}': PageTransition.makePageTransition( - page: const LoginPageView(), - settings: settings, + '/${NavigationItem.navProfile.route}': + MaterialPageRoute( + builder: (__) => const ProfilePageView(), + ), + '/${NavigationItem.navSettings.route}': + MaterialPageRoute( + builder: (_) => const SettingsPage(), ), - '/${DrawerItem.navLogOut.title}': - NavigationService.buildLogoutRoute(), }; return transitions[settings.name]; }, diff --git a/uni/lib/model/entities/calendar_event.dart b/uni/lib/model/entities/calendar_event.dart index 733a3909a..2d96d895e 100644 --- a/uni/lib/model/entities/calendar_event.dart +++ b/uni/lib/model/entities/calendar_event.dart @@ -1,7 +1,12 @@ +import 'package:intl/intl.dart'; + /// An event in the school calendar class CalendarEvent { /// Creates an instance of the class [CalendarEvent] - CalendarEvent(this.name, this.date); + CalendarEvent(this.name, this.date) { + name = name; + date = date; + } String name; String date; @@ -9,4 +14,20 @@ class CalendarEvent { Map toMap() { return {'name': name, 'date': date}; } + + DateTime? get parsedStartDate { + final splitDate = date.split(' '); + final month = splitDate.firstWhere( + (element) => + DateFormat.MMMM('pt').dateSymbols.MONTHS.contains(element) || + element == 'TBD', + ); + + try { + return DateFormat('dd MMMM yyyy', 'pt') + .parse('${splitDate[0]} $month ${splitDate.last}'); + } catch (e) { + return null; + } + } } diff --git a/uni/lib/model/entities/course_units/course_unit_directory.dart b/uni/lib/model/entities/course_units/course_unit_directory.dart new file mode 100644 index 000000000..d14fa6b13 --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_directory.dart @@ -0,0 +1,8 @@ +import 'package:uni/model/entities/course_units/course_unit_file.dart'; + +class CourseUnitFileDirectory { + CourseUnitFileDirectory(this.folderName, this.files); + + final String folderName; + final List files; +} diff --git a/uni/lib/model/entities/course_units/course_unit_file.dart b/uni/lib/model/entities/course_units/course_unit_file.dart new file mode 100644 index 000000000..e7076df1d --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_file.dart @@ -0,0 +1,10 @@ +class CourseUnitFile { + CourseUnitFile( + this.name, + this.url, + this.fileCode, + ); + String fileCode; + String name; + String url; +} diff --git a/uni/lib/model/entities/exam.dart b/uni/lib/model/entities/exam.dart index 0d27cb1bc..466902014 100644 --- a/uni/lib/model/entities/exam.dart +++ b/uni/lib/model/entities/exam.dart @@ -83,12 +83,13 @@ class Exam { @override String toString() { - return '''$id - $subject - ${begin.year} - $month - ${begin.day} - $beginTime-$endTime - $type - $rooms - $weekDay'''; + return '''$id - $subject - ${begin.year} - $month - ${begin.day} - $beginTime-$endTime - $type - $rooms - $weekDay'''; } @override bool operator ==(Object other) => - identical(this, other) || other is Exam && id == other.id; + identical(this, other) || + other is Exam && id == other.id && subject == other.subject; @override int get hashCode => id.hashCode; diff --git a/uni/lib/model/entities/locations/special_room_location.dart b/uni/lib/model/entities/locations/special_room_location.dart index c4cd1013a..0a7c3930e 100644 --- a/uni/lib/model/entities/locations/special_room_location.dart +++ b/uni/lib/model/entities/locations/special_room_location.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:uni/model/entities/location.dart'; class SpecialRoomLocation implements Location { diff --git a/uni/lib/model/entities/locations/store_location.dart b/uni/lib/model/entities/locations/store_location.dart index b66ea07ae..07212d049 100644 --- a/uni/lib/model/entities/locations/store_location.dart +++ b/uni/lib/model/entities/locations/store_location.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:uni/model/entities/location.dart'; class StoreLocation implements Location { diff --git a/uni/lib/model/entities/locations/unknown_location.dart b/uni/lib/model/entities/locations/unknown_location.dart index 121ab3e4d..33ea18f06 100644 --- a/uni/lib/model/entities/locations/unknown_location.dart +++ b/uni/lib/model/entities/locations/unknown_location.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:uni/model/entities/location.dart'; class UnknownLocation implements Location { diff --git a/uni/lib/model/providers/lazy/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart index 2b9f7c36b..ddd90128c 100644 --- a/uni/lib/model/providers/lazy/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -1,86 +1,79 @@ import 'dart:async'; -import 'dart:collection'; import 'package:uni/controller/fetchers/departures_fetcher.dart'; -import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; +import 'package:uni/controller/local_storage/database/app_bus_stop_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/providers/state_providers.dart'; -class BusStopProvider extends StateProviderNotifier { - BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); - Map _configuredBusStops = Map.identity(); - DateTime _timeStamp = DateTime.now(); - - UnmodifiableMapView get configuredBusStops => - UnmodifiableMapView(_configuredBusStops); - - DateTime get timeStamp => _timeStamp; +class BusStopProvider extends StateProviderNotifier> { + BusStopProvider() : super(cacheDuration: null, dependsOnSession: false); @override - Future loadFromStorage() async { + Future> loadFromStorage( + StateProviders stateProviders, + ) { final busStopsDb = AppBusStopDatabase(); - final stops = await busStopsDb.busStops(); - _configuredBusStops = stops; + return busStopsDb.busStops(); } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchUserBusTrips(); + Future> loadFromRemote( + StateProviders stateProviders, + ) async { + return fetchUserBusTrips(state!); } - Future fetchUserBusTrips() async { - for (final stopCode in configuredBusStops.keys) { + Future> fetchUserBusTrips( + Map currentStops, + ) async { + for (final stopCode in currentStops.keys) { final stopTrips = await DeparturesFetcher.getNextArrivalsStop( stopCode, - configuredBusStops[stopCode]!, + currentStops[stopCode]!, ); - _configuredBusStops[stopCode]?.trips = stopTrips; + currentStops[stopCode]?.trips = stopTrips; } - _timeStamp = DateTime.now(); + return currentStops; } Future addUserBusStop(String stopCode, BusStopData stopData) async { - if (_configuredBusStops.containsKey(stopCode)) { - _configuredBusStops[stopCode]!.configuredBuses.clear(); - _configuredBusStops[stopCode]! - .configuredBuses - .addAll(stopData.configuredBuses); + if (state!.containsKey(stopCode)) { + state![stopCode]!.configuredBuses.clear(); + state![stopCode]!.configuredBuses.addAll(stopData.configuredBuses); } else { - _configuredBusStops[stopCode] = stopData; + state![stopCode] = stopData; } notifyListeners(); - await fetchUserBusTrips(); + await fetchUserBusTrips(state!); notifyListeners(); final db = AppBusStopDatabase(); - await db.setBusStops(configuredBusStops); + await db.setBusStops(state!); } Future removeUserBusStop( String stopCode, ) async { - _configuredBusStops.remove(stopCode); + state!.remove(stopCode); notifyListeners(); - await fetchUserBusTrips(); + await fetchUserBusTrips(state!); notifyListeners(); final db = AppBusStopDatabase(); - await db.setBusStops(_configuredBusStops); + await db.setBusStops(state!); } Future toggleFavoriteUserBusStop( String stopCode, BusStopData stopData, ) async { - _configuredBusStops[stopCode]!.favorited = - !_configuredBusStops[stopCode]!.favorited; + state![stopCode]!.favorited = !state![stopCode]!.favorited; notifyListeners(); - await fetchUserBusTrips(); + await fetchUserBusTrips(state!); notifyListeners(); final db = AppBusStopDatabase(); diff --git a/uni/lib/model/providers/lazy/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart index 3e532b718..9884118ae 100644 --- a/uni/lib/model/providers/lazy/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -1,35 +1,30 @@ import 'dart:async'; -import 'dart:collection'; import 'package:uni/controller/fetchers/calendar_fetcher_html.dart'; -import 'package:uni/controller/local_storage/app_calendar_database.dart'; +import 'package:uni/controller/local_storage/database/app_calendar_database.dart'; import 'package:uni/model/entities/calendar_event.dart'; -import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/providers/state_providers.dart'; -class CalendarProvider extends StateProviderNotifier { - CalendarProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(days: 30)); - List _calendar = []; - - UnmodifiableListView get calendar => - UnmodifiableListView(_calendar); +class CalendarProvider extends StateProviderNotifier> { + CalendarProvider() : super(cacheDuration: const Duration(days: 30)); @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchCalendar(session); - } - - Future fetchCalendar(Session session) async { - _calendar = await CalendarFetcherHtml().getCalendar(session); + Future> loadFromStorage( + StateProviders stateProviders, + ) async { final db = CalendarDatabase(); - unawaited(db.saveCalendar(calendar)); + return db.calendar(); } @override - Future loadFromStorage() async { + Future> loadFromRemote( + StateProviders stateProviders, + ) async { + final session = stateProviders.sessionProvider.state!; + final calendar = await CalendarFetcherHtml().getCalendar(session); final db = CalendarDatabase(); - _calendar = await db.calendar(); + unawaited(db.saveCalendar(calendar)); + return calendar; } } diff --git a/uni/lib/model/providers/lazy/course_units_info_provider.dart b/uni/lib/model/providers/lazy/course_units_info_provider.dart index 2a9901870..69d653bfd 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -1,46 +1,77 @@ import 'dart:collection'; +import 'package:tuple/tuple.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_directory.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/providers/state_providers.dart'; -class CourseUnitsInfoProvider extends StateProviderNotifier { +typedef SheetsMap = Map; +typedef ClassesMap = Map>; +typedef FilesMap = Map>; + +class CourseUnitsInfoProvider + extends StateProviderNotifier> { CourseUnitsInfoProvider() - : super(dependsOnSession: true, cacheDuration: null, initialize: false); - final Map _courseUnitsSheets = {}; - final Map> _courseUnitsClasses = {}; + : super( + cacheDuration: null, + // Const constructor is not allowed here because of the + // need for mutable maps + // ignore: prefer_const_constructors + initialState: Tuple3({}, {}, {}), + ); UnmodifiableMapView get courseUnitsSheets => - UnmodifiableMapView(_courseUnitsSheets); + UnmodifiableMapView(state!.item1); UnmodifiableMapView> - get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); + get courseUnitsClasses => UnmodifiableMapView(state!.item2); + + UnmodifiableMapView> + get courseUnitsFiles => UnmodifiableMapView(state!.item3); Future fetchCourseUnitSheet( CourseUnit courseUnit, Session session, ) async { - _courseUnitsSheets[courseUnit] = await CourseUnitsInfoFetcher() + state!.item1[courseUnit] = await CourseUnitsInfoFetcher() .fetchCourseUnitSheet(session, courseUnit.occurrId); + notifyListeners(); } Future fetchCourseUnitClasses( CourseUnit courseUnit, Session session, ) async { - _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() + state!.item2[courseUnit] = await CourseUnitsInfoFetcher() .fetchCourseUnitClasses(session, courseUnit.occurrId); + notifyListeners(); + } + + Future fetchCourseUnitFiles( + CourseUnit courseUnit, + Session session, + ) async { + state!.item3[courseUnit] = await CourseUnitsInfoFetcher() + .fetchCourseUnitFiles(session, courseUnit.occurrId); + notifyListeners(); } @override - Future loadFromRemote(Session session, Profile profile) async { - // Course units info is loaded on demand by its detail page + Future> loadFromRemote( + StateProviders stateProviders, + ) async { + return const Tuple3({}, {}, {}); } @override - Future loadFromStorage() async {} + Future> loadFromStorage( + StateProviders stateProviders, + ) async { + return const Tuple3({}, {}, {}); + } } diff --git a/uni/lib/model/providers/lazy/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart index 5f93c548a..b59aa4c7b 100644 --- a/uni/lib/model/providers/lazy/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -1,54 +1,41 @@ import 'dart:async'; -import 'dart:collection'; 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/local_storage/database/app_exams_database.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/parsers/parser_exams.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/providers/state_providers.dart'; -class ExamProvider extends StateProviderNotifier { - ExamProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); - List _exams = []; - List _hiddenExams = []; - Map _filteredExamsTypes = {}; - - UnmodifiableListView get exams => UnmodifiableListView(_exams); - - UnmodifiableListView get hiddenExams => - UnmodifiableListView(_hiddenExams); - - UnmodifiableMapView get filteredExamsTypes => - UnmodifiableMapView(_filteredExamsTypes); +class ExamProvider extends StateProviderNotifier> { + ExamProvider() : super(cacheDuration: const Duration(days: 1)); @override - Future loadFromStorage() async { - await setFilteredExams(await AppSharedPreferences.getFilteredExams()); - await setHiddenExams(await AppSharedPreferences.getHiddenExams()); - + Future> loadFromStorage(StateProviders stateProviders) async { final db = AppExamsDatabase(); - final exams = await db.exams(); - _exams = exams; + return db.exams(); } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchUserExams( + Future> loadFromRemote(StateProviders stateProviders) async { + final session = stateProviders.sessionProvider.state!; + final profile = stateProviders.profileProvider.state!; + + return fetchUserExams( ParserExams(), profile, session, profile.courseUnits, persistentSession: - (await AppSharedPreferences.getPersistentUserInfo()) != null, + (PreferencesController.getPersistentUserInfo()) != null, ); } - Future fetchUserExams( + Future> fetchUserExams( ParserExams parserExams, Profile profile, Session session, @@ -64,45 +51,6 @@ class ExamProvider extends StateProviderNotifier { await AppExamsDatabase().saveNewExams(exams); } - _exams = exams; - } - - Future updateFilteredExams() async { - final exams = await AppSharedPreferences.getFilteredExams(); - _filteredExamsTypes = exams; - notifyListeners(); - } - - Future setFilteredExams(Map newFilteredExams) async { - unawaited(AppSharedPreferences.saveFilteredExams(newFilteredExams)); - _filteredExamsTypes = Map.from(newFilteredExams); - notifyListeners(); - } - - List getFilteredExams() { - return exams - .where( - (exam) => filteredExamsTypes[Exam.getExamTypeLong(exam.type)] ?? true, - ) - .toList(); - } - - Future setHiddenExams(List newHiddenExams) async { - _hiddenExams = List.from(newHiddenExams); - await AppSharedPreferences.saveHiddenExams(hiddenExams); - notifyListeners(); - } - - Future toggleHiddenExam(String newExamId) async { - _hiddenExams.contains(newExamId) - ? _hiddenExams.remove(newExamId) - : _hiddenExams.add(newExamId); - await AppSharedPreferences.saveHiddenExams(hiddenExams); - notifyListeners(); - } - - set exams(List newExams) { - _exams = newExams; - notifyListeners(); + return exams; } } diff --git a/uni/lib/model/providers/lazy/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart index f43da6743..1dcf63231 100644 --- a/uni/lib/model/providers/lazy/faculty_locations_provider.dart +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -1,24 +1,22 @@ -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/providers/state_providers.dart'; -class FacultyLocationsProvider extends StateProviderNotifier { +class FacultyLocationsProvider + extends StateProviderNotifier> { FacultyLocationsProvider() - : super(dependsOnSession: false, cacheDuration: const Duration(days: 30)); - List _locations = []; - - UnmodifiableListView get locations => - UnmodifiableListView(_locations); + : super(cacheDuration: const Duration(days: 30), dependsOnSession: false); @override - Future loadFromStorage() async { - _locations = await LocationFetcherAsset().getLocations(); + Future> loadFromStorage(StateProviders stateProviders) { + return LocationFetcherAsset().getLocations(); } @override - Future loadFromRemote(Session session, Profile profile) async {} + Future> loadFromRemote( + StateProviders stateProviders, + ) async { + return state!; + } } diff --git a/uni/lib/model/providers/lazy/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart deleted file mode 100644 index 48d162e33..000000000 --- a/uni/lib/model/providers/lazy/home_page_provider.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/model/entities/profile.dart'; -import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/utils/favorite_widget_type.dart'; - -class HomePageProvider extends StateProviderNotifier { - HomePageProvider() : super(dependsOnSession: false, cacheDuration: null); - List _favoriteCards = []; - bool _isEditing = false; - - 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 {} - - void setHomePageEditingMode({required bool editingMode}) { - _isEditing = editingMode; - notifyListeners(); - } - - void toggleHomePageEditingMode() { - _isEditing = !_isEditing; - notifyListeners(); - } - - void setFavoriteCards(List favoriteCards) { - _favoriteCards = favoriteCards; - notifyListeners(); - } -} diff --git a/uni/lib/model/providers/lazy/lecture_provider.dart b/uni/lib/model/providers/lazy/lecture_provider.dart index 93201bdbd..3082bd66e 100644 --- a/uni/lib/model/providers/lazy/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -1,41 +1,36 @@ import 'dart:async'; -import 'dart:collection'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart'; -import 'package:uni/controller/local_storage/app_lectures_database.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/database/app_lectures_database.dart'; +import 'package:uni/controller/local_storage/preferences_controller.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/providers/state_providers.dart'; -class LectureProvider extends StateProviderNotifier { - LectureProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(hours: 6)); - List _lectures = []; - - UnmodifiableListView get lectures => UnmodifiableListView(_lectures); +class LectureProvider extends StateProviderNotifier> { + LectureProvider() : super(cacheDuration: const Duration(hours: 6)); @override - Future loadFromStorage() async { + Future> loadFromStorage(StateProviders stateProviders) async { final db = AppLecturesDatabase(); - final lectures = await db.lectures(); - _lectures = lectures; + return db.lectures(); } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchUserLectures( - session, - profile, + Future> loadFromRemote(StateProviders stateProviders) async { + return fetchUserLectures( + stateProviders.sessionProvider.state!, + stateProviders.profileProvider.state!, persistentSession: - (await AppSharedPreferences.getPersistentUserInfo()) != null, + (PreferencesController.getPersistentUserInfo()) != null, ); } - Future fetchUserLectures( + Future> fetchUserLectures( Session session, Profile profile, { required bool persistentSession, @@ -49,7 +44,7 @@ class LectureProvider extends StateProviderNotifier { await db.saveNewLectures(lectures); } - _lectures = lectures; + return lectures; } Future> getLecturesFromFetcherOrElse( diff --git a/uni/lib/model/providers/lazy/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart index c1fb18199..258acf2e7 100644 --- a/uni/lib/model/providers/lazy/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -1,36 +1,34 @@ import 'dart:async'; import 'package:uni/controller/fetchers/library_occupation_fetcher.dart'; -import 'package:uni/controller/local_storage/app_library_occupation_database.dart'; +import 'package:uni/controller/local_storage/database/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/providers/state_providers.dart'; -class LibraryOccupationProvider extends StateProviderNotifier { - LibraryOccupationProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); - LibraryOccupation? _occupation; - - LibraryOccupation? get occupation => _occupation; +class LibraryOccupationProvider + extends StateProviderNotifier { + LibraryOccupationProvider() : super(cacheDuration: const Duration(hours: 1)); @override - Future loadFromStorage() async { + Future loadFromStorage( + StateProviders stateProviders, + ) async { final db = LibraryOccupationDatabase(); - final occupation = await db.occupation(); - _occupation = occupation; + return db.occupation(); } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchLibraryOccupation(session); - } - - Future fetchLibraryOccupation(Session session) async { - _occupation = await LibraryOccupationFetcherSheets() + Future loadFromRemote( + StateProviders stateProviders, + ) async { + final session = stateProviders.sessionProvider.state!; + final occupation = await LibraryOccupationFetcherSheets() .getLibraryOccupationFromSheets(session); final db = LibraryOccupationDatabase(); - unawaited(db.saveOccupation(_occupation!)); + unawaited(db.saveOccupation(occupation)); + + return occupation; } } diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart index 619090669..013d565ec 100644 --- a/uni/lib/model/providers/lazy/reference_provider.dart +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -1,40 +1,31 @@ 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/local_storage/database/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/providers/state_providers.dart'; -class ReferenceProvider extends StateProviderNotifier { - ReferenceProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); - List _references = []; - - UnmodifiableListView get references => - UnmodifiableListView(_references); +class ReferenceProvider extends StateProviderNotifier> { + ReferenceProvider() : super(cacheDuration: const Duration(hours: 1)); @override - Future loadFromStorage() async { + Future> loadFromStorage(StateProviders stateProviders) { final referencesDb = AppReferencesDatabase(); - final references = await referencesDb.references(); - _references = references; + return referencesDb.references(); } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchUserReferences(session); - } + Future> loadFromRemote(StateProviders stateProviders) async { + final session = stateProviders.sessionProvider.state!; - Future fetchUserReferences(Session session) async { final response = await ReferenceFetcher().getUserReferenceResponse(session); - - _references = await parseReferences(response); + final references = await parseReferences(response); final referencesDb = AppReferencesDatabase(); unawaited(referencesDb.saveNewReferences(references)); + + return references; } } diff --git a/uni/lib/model/providers/lazy/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart index d0db39f40..0721d3754 100644 --- a/uni/lib/model/providers/lazy/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -1,63 +1,31 @@ import 'dart:async'; -import 'dart:collection'; import 'package:uni/controller/fetchers/restaurant_fetcher.dart'; -import 'package:uni/controller/local_storage/app_restaurant_database.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/model/entities/profile.dart'; +import 'package:uni/controller/local_storage/database/app_restaurant_database.dart'; import 'package:uni/model/entities/restaurant.dart'; -import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/providers/state_providers.dart'; -class RestaurantProvider extends StateProviderNotifier { - RestaurantProvider() - : super(dependsOnSession: false, cacheDuration: const Duration(days: 1)); - - List _restaurants = []; - List _favoriteRestaurants = []; - - UnmodifiableListView get restaurants => - UnmodifiableListView(_restaurants); - - UnmodifiableListView get favoriteRestaurants => - UnmodifiableListView(_favoriteRestaurants); +class RestaurantProvider extends StateProviderNotifier> { + RestaurantProvider() : super(cacheDuration: const Duration(days: 1)); @override - Future loadFromStorage() async { + Future> loadFromStorage( + StateProviders stateProviders, + ) async { final restaurantDb = RestaurantDatabase(); final restaurants = await restaurantDb.getRestaurants(); - _restaurants = restaurants; - _favoriteRestaurants = await AppSharedPreferences.getFavoriteRestaurants(); + return restaurants; } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchRestaurants(session); - } - - Future fetchRestaurants(Session session) async { + Future> loadFromRemote(StateProviders stateProviders) async { + final session = stateProviders.sessionProvider.state!; final restaurants = await RestaurantFetcher().getRestaurants(session); final db = RestaurantDatabase(); unawaited(db.saveRestaurants(restaurants)); - _restaurants = filterPastMeals(restaurants); - } - - Future toggleFavoriteRestaurant( - String restaurantName, - ) async { - _favoriteRestaurants.contains(restaurantName) - ? _favoriteRestaurants.remove(restaurantName) - : _favoriteRestaurants.add(restaurantName); - notifyListeners(); - await AppSharedPreferences.saveFavoriteRestaurants(favoriteRestaurants); - } - - Future updateStateBasedOnLocalRestaurants() async { - final restaurantDb = RestaurantDatabase(); - final restaurants = await restaurantDb.getRestaurants(); - _restaurants = restaurants; - notifyListeners(); + return filterPastMeals(restaurants); } } diff --git a/uni/lib/model/providers/plausible/plausible_provider.dart b/uni/lib/model/providers/plausible/plausible_provider.dart new file mode 100644 index 000000000..e60f13daa --- /dev/null +++ b/uni/lib/model/providers/plausible/plausible_provider.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:battery_plus/battery_plus.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logger/logger.dart'; +import 'package:plausible_analytics/plausible_analytics.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; + +class PlausibleProvider extends StatefulWidget { + const PlausibleProvider({ + required this.plausible, + required this.child, + super.key, + }); + + final Plausible? plausible; + final Widget child; + + @override + State createState() => _PlausibleProviderState(); +} + +class _PlausibleProviderState extends State { + int _batteryLevel = 0; + bool _isInBatterySaveMode = true; + ConnectivityResult _connectivityResult = ConnectivityResult.mobile; + bool _isUsageStatsEnabled = true; + + bool _canUpdateBatteryState = true; + + @override + void initState() { + super.initState(); + + final plausible = widget.plausible; + if (plausible != null) { + plausible.enabled = false; + + _startListeners(plausible) + .then((_) => _updateBatteryState()) + .then((_) => _updateConnectivityState()) + .then((_) => _updateUsageStatsState()); + } + } + + void _updateAnalyticsState() { + final plausible = widget.plausible; + if (plausible != null) { + plausible.enabled = _isUsageStatsEnabled && + !_isInBatterySaveMode && + _batteryLevel > 20 && + _connectivityResult == ConnectivityResult.wifi; + + Logger().d('Plausible enabled: ${plausible.enabled}'); + } + } + + Future _updateBatteryState() async { + if (!_canUpdateBatteryState) { + return; + } + + _canUpdateBatteryState = false; + Timer(const Duration(minutes: 1), () => _canUpdateBatteryState = true); + + final battery = Battery(); + _batteryLevel = await battery.batteryLevel; + _isInBatterySaveMode = await battery.isInBatterySaveMode; + + _updateAnalyticsState(); + } + + Future _updateConnectivityState() async { + final connectivity = Connectivity(); + _connectivityResult = await connectivity.checkConnectivity(); + + _updateAnalyticsState(); + } + + Future _updateUsageStatsState() async { + _isUsageStatsEnabled = PreferencesController.getUsageStatsToggle(); + _updateAnalyticsState(); + } + + Future _startListeners(Plausible plausible) async { + final connectivity = Connectivity(); + connectivity.onConnectivityChanged.listen((result) { + _connectivityResult = result; + _updateAnalyticsState(); + }); + + PreferencesController.onStatsToggle.listen((event) { + _isUsageStatsEnabled = event; + _updateAnalyticsState(); + }); + } + + @override + Widget build(BuildContext context) { + return Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + final plausible = widget.plausible; + if (plausible != null) { + _updateBatteryState(); + } + }, + child: Provider.value( + value: widget.plausible, + child: widget.child, + ), + ); + } +} diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index 349c3d926..0095059f3 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -1,117 +1,152 @@ import 'dart:async'; import 'dart:io'; +import 'package:tuple/tuple.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/all_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart'; import 'package:uni/controller/fetchers/fees_fetcher.dart'; import 'package:uni/controller/fetchers/print_fetcher.dart'; import 'package:uni/controller/fetchers/profile_fetcher.dart'; -import 'package:uni/controller/local_storage/app_course_units_database.dart'; -import 'package:uni/controller/local_storage/app_courses_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/database/app_course_units_database.dart'; +import 'package:uni/controller/local_storage/database/app_courses_database.dart'; +import 'package:uni/controller/local_storage/database/app_user_database.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; +import 'package:uni/model/entities/course.dart'; +import 'package:uni/model/entities/course_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/providers/state_providers.dart'; -class ProfileProvider extends StateProviderNotifier { +class ProfileProvider extends StateProviderNotifier { ProfileProvider() - : _profile = Profile(), - super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); - Profile _profile; - - Profile get profile => _profile; + : super(cacheDuration: const Duration(days: 1), dependsOnSession: false); @override - Future loadFromStorage() async { - await loadProfile(); - await Future.wait( - [loadCourses(), loadCourseUnits()], - ); + Future loadFromStorage(StateProviders stateProviders) async { + final databaseFutures = await Future.wait([ + loadProfile(), + loadCourses(), + loadCourseUnits(), + ]); + + final profile = databaseFutures[0] as Profile; + final courses = databaseFutures[1] as List; + final courseUnits = databaseFutures[2] as List; + + profile + ..courses = courses + ..courseUnits = courseUnits; + + return profile; } @override - Future loadFromRemote(Session session, Profile profile) async { - await fetchUserInfo(session); + Future loadFromRemote(StateProviders stateProviders) async { + final session = stateProviders.sessionProvider.state!; + + final profileFuture = fetchUserInfo(session); + final courseUnitsFutures = profileFuture.then( + (profile) => fetchCourseUnitsAndCourseAverages(session, profile!), + ); - await Future.wait([ - fetchUserFees(session), + final futures = await Future.wait([ + profileFuture, + courseUnitsFutures, + fetchUserFeesBalanceAndLimit(session), fetchUserPrintBalance(session), - fetchCourseUnitsAndCourseAverages(session), ]); + final profile = futures[0] as Profile?; + final courseUnits = futures[1] as List?; + final userBalanceAndFeesLimit = futures[2]! as Tuple2; + final printBalance = futures[3]! as String; + + profile! + ..feesBalance = userBalanceAndFeesLimit.item1 + ..feesLimit = userBalanceAndFeesLimit.item2 + ..printBalance = printBalance; + + if (courseUnits != null) { + profile.courseUnits = courseUnits; + } + + return profile; } - Future loadProfile() async { + Future loadProfile() { final profileDb = AppUserDataDatabase(); - _profile = await profileDb.getUserData(); + return profileDb.getUserData(); } - Future loadCourses() async { + Future> loadCourses() { final coursesDb = AppCoursesDatabase(); - final courses = await coursesDb.courses(); - _profile.courses = courses; + return coursesDb.courses(); } - Future loadCourseUnits() async { + Future> loadCourseUnits() { final db = AppCourseUnitsDatabase(); - profile.courseUnits = await db.courseUnits(); + return db.courseUnits(); } - Future fetchUserFees(Session session) async { + Future> fetchUserFeesBalanceAndLimit( + Session session, + ) async { final response = await FeesFetcher().getUserFeesResponse(session); final feesBalance = parseFeesBalance(response); final feesLimit = parseFeesNextLimit(response); - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); if (userPersistentInfo != null) { final profileDb = AppUserDataDatabase(); await profileDb.saveUserFees(feesBalance, feesLimit); } - _profile - ..feesBalance = feesBalance - ..feesLimit = feesLimit; + return Tuple2(feesBalance, feesLimit); } - Future fetchUserPrintBalance(Session session) async { + Future fetchUserPrintBalance(Session session) async { final response = await PrintFetcher().getUserPrintsResponse(session); final printBalance = await getPrintsBalance(response); - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); if (userPersistentInfo != null) { final profileDb = AppUserDataDatabase(); await profileDb.saveUserPrintBalance(printBalance); } - _profile.printBalance = printBalance; + return printBalance; } - Future fetchUserInfo(Session session) async { + Future fetchUserInfo(Session session) async { final profile = await ProfileFetcher.fetchProfile(session); + if (profile == null) { + return null; + } + final currentCourseUnits = await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); - _profile = profile ?? Profile(); - _profile.courseUnits = currentCourseUnits; + profile.courseUnits = currentCourseUnits; - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); if (userPersistentInfo != null) { // Course units are saved later, so we don't it here final profileDb = AppUserDataDatabase(); - await profileDb.insertUserData(_profile); + await profileDb.insertUserData(profile); } + + return profile; } - Future fetchCourseUnitsAndCourseAverages(Session session) async { - final courses = profile.courses; + Future?> fetchCourseUnitsAndCourseAverages( + Session session, + Profile profile, + ) async { final allCourseUnits = await AllCourseUnitsFetcher().getAllCourseUnitsAndCourseAverages( profile.courses, @@ -119,22 +154,20 @@ class ProfileProvider extends StateProviderNotifier { currentCourseUnits: profile.courseUnits, ); - if (allCourseUnits != null) { - _profile.courseUnits = allCourseUnits; - } else { - // Current course units should already have been fetched, - // so this is not a fatal error + if (allCourseUnits == null) { + return allCourseUnits; } - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); if (userPersistentInfo != null) { final coursesDb = AppCoursesDatabase(); - await coursesDb.saveNewCourses(courses); + unawaited(coursesDb.saveNewCourses(profile.courses)); final courseUnitsDatabase = AppCourseUnitsDatabase(); - await courseUnitsDatabase.saveNewCourseUnits(_profile.courseUnits); + unawaited(courseUnitsDatabase.saveNewCourseUnits(allCourseUnits)); } + + return allCourseUnits; } static Future fetchOrGetCachedProfilePicture( diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 84cbca043..037392a37 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -3,91 +3,86 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; 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/fetchers/faculties_fetcher.dart'; +import 'package:uni/controller/fetchers/terms_and_conditions_fetcher.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_session.dart'; import 'package:uni/model/entities/login_exceptions.dart'; -import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/providers/state_providers.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/view/locale_notifier.dart'; -class SessionProvider extends StateProviderNotifier { +class SessionProvider extends StateProviderNotifier { SessionProvider() - : _session = Session( - faculties: ['feup'], - username: '', - cookies: '', - ), - super( - dependsOnSession: false, + : super( cacheDuration: null, initialStatus: RequestStatus.none, + dependsOnSession: false, ); - Session _session; - - Session get session => _session; - @override - Future loadFromStorage() async { - final userPersistentInfo = - await AppSharedPreferences.getPersistentUserInfo(); + Future loadFromStorage(StateProviders stateProviders) async { + final userPersistentInfo = PreferencesController.getPersistentUserInfo(); + final faculties = PreferencesController.getUserFaculties(); if (userPersistentInfo == null) { - return; + return Session(username: '', cookies: '', faculties: faculties); } - final userName = userPersistentInfo.item1; - final password = userPersistentInfo.item2; - - final faculties = await AppSharedPreferences.getUserFaculties(); - - restoreSession(userName, password, faculties); - } - - @override - Future loadFromRemote(Session session, Profile profile) async {} - - void restoreSession( - String username, - String password, - List faculties, - ) { - _session = Session( + return Session( faculties: faculties, - username: username, + username: userPersistentInfo.item1, cookies: '', persistentSession: true, ); } + @override + Future loadFromRemote(StateProviders stateProviders) async { + return state!; + } + Future postAuthentication( BuildContext context, String username, - String password, - List faculties, { + String password, { required bool persistentSession, }) async { final locale = Provider.of(context, listen: false).getLocale(); Session? session; + List faculties; + try { + // We need to login to fetch the faculties, so perform a temporary login. + final tempSession = await NetworkRouter.login( + username, + password, + ['feup'], + persistentSession: false, + ignoreCached: true, + ); + faculties = await getStudentFaculties(tempSession!); + + // Now get the session with the correct faculties. session = await NetworkRouter.login( username, password, faculties, persistentSession: persistentSession, + ignoreCached: true, ); } catch (e) { throw InternetStatusException(locale); } if (session == null) { + // Get the fail reason. final responseHtml = - await NetworkRouter.loginInSigarra(username, password, faculties); + await NetworkRouter.loginInSigarra(username, password, ['feup']); if (isPasswordExpired(responseHtml) && context.mounted) { throw ExpiredCredentialsException(); @@ -98,10 +93,10 @@ class SessionProvider extends StateProviderNotifier { } } - _session = session; + setState(session); if (persistentSession) { - await AppSharedPreferences.savePersistentUserInfo( + await PreferencesController.savePersistentUserInfo( session.username, password, faculties, diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index cc248712e..3eff8fd5d 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -1,87 +1,103 @@ +import 'dart:async'; + import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; -import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:synchronized/synchronized.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; -import 'package:uni/model/entities/profile.dart'; -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/controller/local_storage/preferences_controller.dart'; +import 'package:uni/model/providers/state_providers.dart'; import 'package:uni/model/request_status.dart'; -abstract class StateProviderNotifier extends ChangeNotifier { +abstract class StateProviderNotifier extends ChangeNotifier { StateProviderNotifier({ required this.cacheDuration, this.dependsOnSession = true, RequestStatus initialStatus = RequestStatus.busy, - bool initialize = true, - }) : _initialStatus = initialStatus, - _status = initialStatus, - _initializedFromStorage = !initialize, - _initializedFromRemote = !initialize; + T? initialState, + }) : _requestStatus = initialStatus, + _state = initialState; + + /// The model that this notifier provides. + /// This future will throw if the data loading fails. + T? _state; + + /// Whether this provider depends on Session and Profile to fetch data. + bool dependsOnSession; + + /// The data loading request status. + RequestStatus _requestStatus; + + /// The timeout for concurrent state change operations. + static const _lockTimeout = Duration(seconds: 30); - static const lockTimeout = Duration(seconds: 30); + /// The lock for concurrent state change operations. final Lock _lock = Lock(); - final RequestStatus _initialStatus; - RequestStatus _status; - bool _initializedFromStorage; - bool _initializedFromRemote; + + /// The last time the model was fetched from the remote. DateTime? _lastUpdateTime; - bool dependsOnSession; + + /// The maximum time after the last update from the remote + /// to retrieve cached data. Duration? cacheDuration; - RequestStatus get status => _status; + RequestStatus get requestStatus => _requestStatus; + + T? get state => _state; DateTime? get lastUpdateTime => _lastUpdateTime; - void markAsInitialized() { - _initializedFromStorage = true; - _initializedFromRemote = true; - _status = RequestStatus.successful; - _lastUpdateTime = DateTime.now(); - notifyListeners(); - } + /// Gets the model from the local database. + /// This method such not catch data loading errors. + Future loadFromStorage(StateProviders stateProviders); - void markAsNotInitialized() { - _initializedFromStorage = false; - _initializedFromRemote = false; - _status = _initialStatus; - _lastUpdateTime = null; + /// Gets the model from the remote server. + /// This will run once when the provider is first initialized. + /// This method must not catch data loading errors. + /// This method should save data in the database, if appropriate. + Future loadFromRemote(StateProviders stateProviders); + + /// Update the current model state, notifying the listeners. + /// This should be called only to modify the model after + /// it has been loaded, for example as a UI callback side effect. + void setState(T newState) { + _state = newState; + notifyListeners(); } - void _updateStatus(RequestStatus status) { - _status = status; - notifyListeners(); + /// Makes the state null, as if the model has never been loaded, + /// so that consumers may trigger the loading again. + void invalidate() { + _state = null; } - Future _loadFromStorage() async { + Future _loadFromStorage(BuildContext context) async { Logger().d('Loading $runtimeType info from storage'); - _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( + _lastUpdateTime = PreferencesController.getLastDataClassUpdateTime( runtimeType.toString(), ); try { - await loadFromStorage(); - notifyListeners(); + setState(await loadFromStorage(StateProviders.fromContext(context))); } catch (e, stackTrace) { await Sentry.captureException(e, stackTrace: stackTrace); Logger() .e('Failed to load $runtimeType info from storage: $e\n$stackTrace'); + _updateStatus(RequestStatus.failed); } Logger().i('Loaded $runtimeType info from storage'); } - Future _loadFromRemote( - Session session, - Profile profile, { + Future _loadFromRemoteFromContext( + BuildContext context, { bool force = false, }) async { Logger().d('Loading $runtimeType info from remote'); + _updateStatus(RequestStatus.busy); + final shouldReload = force || _lastUpdateTime == null || cacheDuration == null || @@ -103,19 +119,21 @@ abstract class StateProviderNotifier extends ChangeNotifier { return; } - _updateStatus(RequestStatus.busy); - try { - await loadFromRemote(session, profile); + if (!context.mounted) { + return; + } + setState(await loadFromRemote(StateProviders.fromContext(context))); Logger().i('Loaded $runtimeType info from remote'); _lastUpdateTime = DateTime.now(); - _updateStatus(RequestStatus.successful); - await AppSharedPreferences.setLastDataClassUpdateTime( + await PreferencesController.setLastDataClassUpdateTime( runtimeType.toString(), _lastUpdateTime!, ); + + _updateStatus(RequestStatus.successful); } catch (e, stackTrace) { await Sentry.captureException(e, stackTrace: stackTrace); Logger() @@ -124,68 +142,33 @@ abstract class StateProviderNotifier extends ChangeNotifier { } } + void _updateStatus(RequestStatus newStatus) { + _requestStatus = newStatus; + notifyListeners(); + } + Future forceRefresh(BuildContext context) async { await _lock.synchronized( () async { if (!context.mounted) { return; } - final session = context.read().session; - final profile = context.read().profile; - _updateStatus(RequestStatus.busy); - await _loadFromRemote(session, profile, force: true); + await _loadFromRemoteFromContext(context, force: true); }, - timeout: lockTimeout, + timeout: _lockTimeout, ); } Future ensureInitialized(BuildContext context) async { - await ensureInitializedFromStorage(); - - if (context.mounted) { - await ensureInitializedFromRemote(context); - } - } - - Future ensureInitializedFromRemote(BuildContext context) async { - await _lock.synchronized( - () async { - if (_initializedFromRemote || !context.mounted) { - return; - } - - final session = context.read().session; - final profile = context.read().profile; - - _initializedFromRemote = true; - - await _loadFromRemote(session, profile); - }, - timeout: lockTimeout, - ); - } - - /// 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) { + if (!context.mounted || _state != null) { return; } - - _initializedFromStorage = true; - await _loadFromStorage(); + await _loadFromStorage(context) + .then((value) => _loadFromRemoteFromContext(context)); }, - timeout: lockTimeout, + timeout: _lockTimeout, ); } - - Future loadFromStorage(); - - /// Loads data from the remote server into the provider. - /// This will run once when the provider is first initialized. - /// This method must not catch data loading errors. - 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 d0d6dc5cb..96a94dd26 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; @@ -5,7 +7,6 @@ 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'; @@ -25,11 +26,28 @@ class StateProviders { this.calendarProvider, this.libraryOccupationProvider, this.facultyLocationsProvider, - this.homePageProvider, this.referenceProvider, ); factory StateProviders.fromContext(BuildContext context) { + // In tests, one does not initialize all providers + // but a version of them is needed for the load methods. + if (Platform.environment.containsKey('FLUTTER_TEST')) { + return StateProviders( + LectureProvider(), + ExamProvider(), + BusStopProvider(), + RestaurantProvider(), + ProfileProvider(), + CourseUnitsInfoProvider(), + SessionProvider(), + CalendarProvider(), + LibraryOccupationProvider(), + FacultyLocationsProvider(), + ReferenceProvider(), + ); + } + final lectureProvider = Provider.of(context, listen: false); final examProvider = Provider.of(context, listen: false); @@ -49,8 +67,6 @@ class StateProviders { Provider.of(context, listen: false); final facultyLocationsProvider = Provider.of(context, listen: false); - final homePageProvider = - Provider.of(context, listen: false); final referenceProvider = Provider.of(context, listen: false); @@ -65,7 +81,6 @@ class StateProviders { calendarProvider, libraryOccupationProvider, facultyLocationsProvider, - homePageProvider, referenceProvider, ); } @@ -80,21 +95,19 @@ class StateProviders { final CalendarProvider calendarProvider; final LibraryOccupationProvider libraryOccupationProvider; final FacultyLocationsProvider facultyLocationsProvider; - final HomePageProvider homePageProvider; final ReferenceProvider referenceProvider; - void markAsNotInitialized() { - lectureProvider.markAsNotInitialized(); - examProvider.markAsNotInitialized(); - busStopProvider.markAsNotInitialized(); - restaurantProvider.markAsNotInitialized(); - courseUnitsInfoProvider.markAsNotInitialized(); - profileProvider.markAsNotInitialized(); - sessionProvider.markAsNotInitialized(); - calendarProvider.markAsNotInitialized(); - libraryOccupationProvider.markAsNotInitialized(); - facultyLocationsProvider.markAsNotInitialized(); - homePageProvider.markAsNotInitialized(); - referenceProvider.markAsNotInitialized(); + void invalidate() { + lectureProvider.invalidate(); + examProvider.invalidate(); + busStopProvider.invalidate(); + restaurantProvider.invalidate(); + courseUnitsInfoProvider.invalidate(); + profileProvider.invalidate(); + sessionProvider.invalidate(); + calendarProvider.invalidate(); + libraryOccupationProvider.invalidate(); + facultyLocationsProvider.invalidate(); + referenceProvider.invalidate(); } } diff --git a/uni/lib/model/utils/time/week.dart b/uni/lib/model/utils/time/week.dart new file mode 100644 index 000000000..02f8efe26 --- /dev/null +++ b/uni/lib/model/utils/time/week.dart @@ -0,0 +1,121 @@ +/// A [Week] represents a period of 7 days. +class Week implements Comparable { + /// Creates a [Week] that starts the given [start] **date** (not datetime). + factory Week({ + required DateTime start, + }) { + final startAtMidnight = start.copyWith( + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ); + + final end = startAtMidnight.add(const Duration(days: 7)); + + return Week._internal(startAtMidnight, end); + } + + // Recommended by https://dart.dev/language/constructors#factory-constructors + Week._internal(this.start, this.end); + + final DateTime start; + final DateTime end; + + /// Returns whether the given [date] is within this [Week]. + bool contains(DateTime date) { + // First check if is at the same time or after the start of the week. + // Then check if is before the (exclusive) end of the week. + return date.compareTo(start) >= 0 && date.isBefore(end); + } + + /// Returns the [Week] that starts at the end of this [Week]. + Week next() { + return Week._internal(end, end.add(const Duration(days: 7))); + } + + /// Returns the [Week] that ends at the start of this [Week]. + Week previous() { + return Week._internal(start.subtract(const Duration(days: 7)), start); + } + + /// Returns the [Week] that is [duration] before this week. + Week subtract(Duration duration) { + final normalizedDuration = Duration(days: duration.inDays); + return Week._internal( + start.subtract(normalizedDuration), + end.subtract(normalizedDuration), + ); + } + + /// Returns the [Week] that is [duration] after this week. + Week add(Duration duration) { + final normalizedDuration = Duration(days: duration.inDays); + return Week._internal( + start.add(normalizedDuration), + end.add(normalizedDuration), + ); + } + + /// Returns the [Week] that starts at the given [weekday], contained in this + /// [Week]. + /// + /// The values for [weekday] are according to [DateTime.weekday]. + Week startingOn(int weekday) { + // For instance, if [weekday] is 1 and [start] is on weekday 3, + // the final offset in days should be 5, since the offset must not be + // negative (the start of the returned week must be contained in this week). + final offsetInDays = (weekday - start.weekday) % 7; + + return Week._internal( + start.add(Duration(days: offsetInDays)), + end.add(Duration(days: offsetInDays)), + ); + } + + /// Returns the [Week] that ends (exclusive) at the given [weekday], contained + /// in this [Week]. + /// + /// The values for [weekday] are according to [DateTime.weekday]. + Week endingOn(int weekday) { + // For instance, if [weekday] is 1 and [end] is on weekday 3, + // the final offset in days should be 2. + final offsetInDays = (end.weekday - weekday) % 7; + + return Week._internal( + start.subtract(Duration(days: offsetInDays)), + end.subtract(Duration(days: offsetInDays)), + ); + } + + /// Returns the [DateTime] at the start of the given [weekday]. + /// + /// The values for [weekday] are according to [DateTime.weekday]. + DateTime getWeekday(int weekday) { + return start.add(Duration(days: (weekday - start.weekday) % 7)); + } + + Iterable get weekdays { + return Iterable.generate(7, (index) => getWeekday(index + 1)); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Week && other.start == start; + } + + @override + int get hashCode => start.hashCode; + + @override + int compareTo(Week other) { + return start.compareTo(other.start); + } + + @override + String toString() { + return 'Week(start: $start, end: $end)'; + } +} diff --git a/uni/lib/model/utils/time/weekday_mapper.dart b/uni/lib/model/utils/time/weekday_mapper.dart new file mode 100644 index 000000000..615a9602d --- /dev/null +++ b/uni/lib/model/utils/time/weekday_mapper.dart @@ -0,0 +1,153 @@ +/// A class that maps weekdays from one system to another. +/// +/// --- +/// # Explanation +/// +/// Consider the following systems: +/// +/// | System | Mon | Tue | Wed | Thu | Fri | Sat | Sun | +/// |--------|-----|-----|-----|-----|-----|-----|-----| +/// | A | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | B | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +/// | C | 1 | 2 | 3 | 4 | 5 | 6 | 0 | +/// | D | 5 | 6 | 0 | 1 | 2 | 3 | 4 | +/// | E | 1 | 2 | 3 | 4 | 5 | -1 | 0 | +/// +/// All of these systems are valid and used in different contexts. This class +/// allows for mapping weekdays from one system to another. +/// +/// As you can see, a system is defined by two parameters: the number of the +/// first day of the week and the number of Monday (can be another day as long +/// as it is the same across all systems). +class WeekdayMapper { + const WeekdayMapper({ + required int fromStart, + required int fromMonday, + required int toStart, + required int toMonday, + }) : _toMonday = toMonday, + _toStart = toStart, + _fromMonday = fromMonday, + _fromStart = fromStart; + + /// Creates a [WeekdayMapper]. + /// + /// [fromStartWeekday] and [toStartWeekday] are the weekdays that correspond + /// to the first day of the week in the `from` and `to` systems, respectively. + /// These values are according to [DateTime.weekday]. + const WeekdayMapper.fromStartWeekdays({ + required int fromStart, + required int fromStartWeekday, + required int toStart, + required int toStartWeekday, + }) : this( + fromStart: fromStart, + fromMonday: (DateTime.monday - fromStartWeekday) % 7 + fromStart, + toStart: toStart, + toMonday: (DateTime.monday - toStartWeekday) % 7 + toStart, + ); + + static const fromSigarraToDart = WeekdayMapper.fromStartWeekdays( + fromStart: 1, + fromStartWeekday: DateTime.sunday, + toStart: 1, + toStartWeekday: DateTime.monday, + ); + + static const fromDartToIndex = WeekdayMapper.fromStartWeekdays( + fromStart: 1, + fromStartWeekday: DateTime.monday, + toStart: 0, + toStartWeekday: DateTime.monday, + ); + + final int _fromStart; + final int _fromMonday; + + final int _toStart; + final int _toMonday; + + WeekdayMapper then(WeekdayMapper other) { + return WeekdayMapper( + fromStart: _fromStart, + fromMonday: _fromMonday, + toStart: other._toStart, + toMonday: other._toMonday, + ); + } + + WeekdayMapper get inverse => WeekdayMapper( + fromStart: _toStart, + fromMonday: _toMonday, + toStart: _fromStart, + toMonday: _fromMonday, + ); + + int map(int fromWeekday) { + // To find the resulting weekday, it goes like this: + // + // 1. The resulting weekday will be `toWeekdayZeroBased + toStart`. + // `toWeekdayZeroBased` corresponds to the resulting weekday in a system + // that is [0, 6]. By adding `toStart`, we are mapping it to the `to` + // system. + // + // 2. The `toWeekdayZeroBased` will be `toWeekdayZeroBasedUnbound % 7`. + // This operation is essential to return a value that is bound within the + // 7 weekdays that a week has. + // + // 3. The `toWeekdayZeroBasedUnbound` will be + // `fromWeekdayZeroBased + mondayIndexOffset`. `fromWeekdayZeroBased` + // corresponds to the provided weekday in a system that is [0, 6]. This + // can be obtained by performing the operation `fromWeekday - fromStart`. + // + // 4. `mondayIndexOffset` corresponds to the number of days that we need + // to advance a monday (or any other day) in the `from` system to get the + // corresponding weekday in the `to` system. This can be obtained by taking + // difference between `toMondayZeroBased` and `fromMondayZeroBased`. These + // two values can be obtained in the same fashion as `fromWeekdayZeroBased`. + // + // Taking these steps into account, we can derive the following formula: + // + // 1. toWeekdayZeroBased + toStart + // 2. toWeekdayZeroBasedUnbound % 7 + toStart + // 3. (fromWeekdayZeroBased + mondayIndexOffset) % 7 + toStart + // 4. (fromWeekday - fromStart + mondayIndexOffset) % 7 + toStart + // 5. (fromWeekday - fromStart + // + toMondayZeroBased - fromMondayZeroBased) % 7 + toStart + // 6. (fromWeekday - fromStart + // + (toMonday - toStart) - fromMondayZeroBased) % 7 + toStart + // 7. (fromWeekday - fromStart + // + (toMonday - toStart) - (fromMonday - fromStart)) % 7 + toStart + // 8. (fromWeekday - fromStart + // + (toMonday - toStart) - (fromMonday - fromStart)) % 7 + toStart + // 9. (fromWeekday - fromStart + // + toMonday - toStart - fromMonday + fromStart) % 7 + toStart + // 10. (fromWeekday + toMonday - toStart - fromMonday) % 7 + toStart + final toWeekdayZeroBased = + (fromWeekday + _toMonday - _toStart - _fromMonday) % 7; + final toWeekday = toWeekdayZeroBased + _toStart; + + return toWeekday; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is WeekdayMapper && + other._fromStart == _fromStart && + other._fromMonday == _fromMonday && + other._toStart == _toStart && + other._toMonday == _toMonday; + } + + @override + int get hashCode => + _fromStart.hashCode ^ + _fromMonday.hashCode ^ + _toStart.hashCode ^ + _toMonday.hashCode; + + @override + String toString() => 'WeekdayMapper(fromStart: $_fromStart, ' + 'fromMonday: $_fromMonday, toStart: $_toStart, toMonday: $_toMonday)'; +} diff --git a/uni/lib/utils/date_time_formatter.dart b/uni/lib/utils/date_time_formatter.dart new file mode 100644 index 000000000..840b4cc91 --- /dev/null +++ b/uni/lib/utils/date_time_formatter.dart @@ -0,0 +1,20 @@ +import 'package:intl/intl.dart'; +import 'package:uni/model/entities/app_locale.dart'; + +extension DateTimeExtensions on DateTime { + String weekDay(AppLocale locale) { + return DateFormat.EEEE(locale.localeCode.languageCode) + .dateSymbols + .WEEKDAYS[weekday % 7]; + } + + String month(AppLocale locale) { + return DateFormat.EEEE(locale.localeCode.languageCode) + .dateSymbols + .MONTHS[this.month - 1]; + } + + String formattedDate(AppLocale locale) { + return DateFormat.MMMMd(locale.localeCode.languageCode).format(this); + } +} diff --git a/uni/lib/utils/navbar_items.dart b/uni/lib/utils/navbar_items.dart new file mode 100644 index 000000000..aeb77a45e --- /dev/null +++ b/uni/lib/utils/navbar_items.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/utils/navigation_items.dart'; + +enum NavbarItem { + navPersonalArea( + Icons.home_outlined, + Icons.home, + NavigationItem.navPersonalArea, + ), + navAcademicPath( + Icons.school_outlined, + Icons.school, + NavigationItem.navAcademicPath, + ), + navRestaurants( + Icons.free_breakfast_outlined, + Icons.free_breakfast, + NavigationItem.navRestaurants, + ), + navFaculty(Icons.domain_outlined, Icons.domain, NavigationItem.navFaculty), + navTransports(Icons.map_outlined, Icons.map, NavigationItem.navTransports); + + const NavbarItem(this.unselectedIcon, this.selectedIcon, this.item); + + final IconData unselectedIcon; + final IconData selectedIcon; + final NavigationItem item; + + BottomNavigationBarItem toUnselectedBottomNavigationBarItem( + BuildContext context, + ) { + return BottomNavigationBarItem( + icon: Icon(unselectedIcon), + label: '', + tooltip: S.of(context).nav_title(item.route), + ); + } + + BottomNavigationBarItem toSelectedBottomNavigationBarItem( + BuildContext context, + ) { + return BottomNavigationBarItem( + icon: Icon(selectedIcon), + label: '', + tooltip: S.of(context).nav_title(item.route), + ); + } + + String get route { + return item.route; + } +} diff --git a/uni/lib/utils/drawer_items.dart b/uni/lib/utils/navigation_items.dart similarity index 59% rename from uni/lib/utils/drawer_items.dart rename to uni/lib/utils/navigation_items.dart index ca70697f0..67078affc 100644 --- a/uni/lib/utils/drawer_items.dart +++ b/uni/lib/utils/navigation_items.dart @@ -1,4 +1,4 @@ -enum DrawerItem { +enum NavigationItem { navPersonalArea('area'), navSchedule('horario'), navExams('exames'), @@ -8,21 +8,18 @@ enum DrawerItem { navRestaurants('restaurantes'), navCalendar('calendario'), navLibrary('biblioteca', faculties: {'feup'}), - navUsefulInfo('uteis', faculties: {'feup'}), - navAbout('sobre'), - navBugReport('bugs'), - navLogIn('Iniciar sessão'), - navLogOut('Terminar sessão'); + navFaculty('faculdade'), + navAcademicPath('percurso_academico'), + navProfile('perfil'), + navSettings('definicoes'), + navTransports('transportes'); - const DrawerItem(this.title, {this.faculties}); - final String title; + const NavigationItem(this.route, {this.faculties}); + + final String route; final Set? faculties; bool isVisible(List userFaculties) { - if (this == DrawerItem.navLogIn || this == DrawerItem.navLogOut) { - return false; - } - if (faculties == null) { return true; } diff --git a/uni/lib/utils/url_parser.dart b/uni/lib/utils/url_parser.dart deleted file mode 100644 index 79d13c6ec..000000000 --- a/uni/lib/utils/url_parser.dart +++ /dev/null @@ -1,26 +0,0 @@ -Map getUrlQueryParameters(String url) { - final queryParameters = {}; - var queryString = url; - - final lastSlashIndex = url.lastIndexOf('/'); - if (lastSlashIndex >= 0) { - queryString = url.substring(lastSlashIndex + 1); - } - - final queryStartIndex = queryString.lastIndexOf('?'); - if (queryStartIndex < 0) { - return {}; - } - queryString = queryString.substring(queryStartIndex + 1); - - final params = queryString.split('&'); - for (final param in params) { - final keyValue = param.split('='); - if (keyValue.length != 2) { - continue; - } - queryParameters[keyValue[0].trim()] = keyValue[1].trim(); - } - - return queryParameters; -} diff --git a/uni/lib/view/about/about.dart b/uni/lib/view/about/about.dart index af9f90eb7..037e195bc 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/about/widgets/terms_and_conditions.dart'; -import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; class AboutPageView extends StatefulWidget { const AboutPageView({super.key}); @@ -11,7 +11,7 @@ class AboutPageView extends StatefulWidget { } /// Manages the 'about' section of the app. -class AboutPageViewState extends GeneralPageViewState { +class AboutPageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { final queryData = MediaQuery.of(context); @@ -37,4 +37,9 @@ class AboutPageViewState extends GeneralPageViewState { @override Future onRefresh(BuildContext context) async {} + + @override + String? getTitle() { + return null; + } } diff --git a/uni/lib/view/about/widgets/terms_and_conditions.dart b/uni/lib/view/about/widgets/terms_and_conditions.dart index 1a53d07da..1943a73f0 100644 --- a/uni/lib/view/about/widgets/terms_and_conditions.dart +++ b/uni/lib/view/about/widgets/terms_and_conditions.dart @@ -1,16 +1,16 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:uni/controller/load_static/terms_and_conditions.dart'; +import 'package:uni/controller/fetchers/terms_and_conditions_fetcher.dart'; +import 'package:uni/controller/networking/url_launcher.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:url_launcher/url_launcher.dart'; class TermsAndConditions extends StatelessWidget { const TermsAndConditions({super.key}); @override Widget build(BuildContext context) { - var termsAndConditionsSaved = S.of(context).loading_terms; + String? termsAndConditionsSaved = S.of(context).loading_terms; final termsAndConditionsFuture = fetchTermsAndConditions(); return FutureBuilder( future: termsAndConditionsFuture, @@ -18,16 +18,14 @@ class TermsAndConditions extends StatelessWidget { (BuildContext context, AsyncSnapshot termsAndConditions) { if (termsAndConditions.connectionState == ConnectionState.done && termsAndConditions.hasData) { - termsAndConditionsSaved = termsAndConditions.data!; + termsAndConditionsSaved = termsAndConditions.data; } return MarkdownBody( styleSheet: MarkdownStyleSheet(), shrinkWrap: false, - data: termsAndConditionsSaved, + data: termsAndConditionsSaved!, onTapLink: (text, url, title) async { - if (await canLaunchUrl(Uri.parse(url!))) { - await launchUrl(Uri.parse(url)); - } + await launchUrlWithToast(context, url!); }, ); }, diff --git a/uni/lib/view/academic_path/academic_path.dart b/uni/lib/view/academic_path/academic_path.dart new file mode 100644 index 000000000..243b6aa08 --- /dev/null +++ b/uni/lib/view/academic_path/academic_path.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/academic_path/widgets/course_units_card.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/home/widgets/exam_card.dart'; +import 'package:uni/view/home/widgets/schedule_card.dart'; + +class AcademicPathPageView extends StatefulWidget { + const AcademicPathPageView({super.key}); + + @override + State createState() => AcademicPathPageViewState(); +} + +class AcademicPathPageViewState extends GeneralPageViewState { + List academicPathCards = [ + ScheduleCard(), + ExamCard(), + CourseUnitsCard(), + // Add more cards if needed + ]; + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navAcademicPath.route); + + @override + Widget getBody(BuildContext context) { + return ListView( + children: academicPathCards, + ); + } + + @override + Future onRefresh(BuildContext context) async { + for (final card in academicPathCards) { + card.onRefresh(context); + } + } +} diff --git a/uni/lib/view/academic_path/widgets/course_units_card.dart b/uni/lib/view/academic_path/widgets/course_units_card.dart new file mode 100644 index 000000000..828f3e43b --- /dev/null +++ b/uni/lib/view/academic_path/widgets/course_units_card.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/course_units/widgets/course_unit_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; + +class CourseUnitsCard extends GenericCard { + CourseUnitsCard({super.key}); + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } + + @override + Widget buildCardContent(BuildContext context) { + return LazyConsumer( + builder: (context, profile) { + final courseUnits = profile.courseUnits + .where( + (courseUnit) => + courseUnit.enrollmentIsValid() && courseUnit.grade == '', + ) + .take(5) + .toList(); + return _generateCourseUnitsCards(courseUnits, context); + }, + hasContent: (Profile profile) => profile.courseUnits.isNotEmpty, + onNullContent: Center( + heightFactor: 10, + child: Text( + S.of(context).no_course_units, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ); + } + + Widget _generateCourseUnitsCards( + List courseUnits, + BuildContext context, + ) { + if (courseUnits.isEmpty) { + return Center( + heightFactor: 3, + child: Text( + S.of(context).no_course_units, + style: Theme.of(context).textTheme.titleLarge, + ), + ); + } + + return Column( + children: courseUnits + .map( + (courseUnit) => Column( + children: [ + Padding( + padding: const EdgeInsets.all(5), + child: CourseUnitCard(courseUnit), + ), + ], + ), + ) + .toList(), + ); + } + + @override + String getTitle(BuildContext context) => + S.of(context).nav_title(NavigationItem.navCourseUnits.route); + + @override + Future onClick(BuildContext context) => + Navigator.pushNamed(context, '/${NavigationItem.navCourseUnits.route}'); +} diff --git a/uni/lib/view/bug_report/bug_report.dart b/uni/lib/view/bug_report/bug_report.dart index f9f7a5c82..413c1859e 100644 --- a/uni/lib/view/bug_report/bug_report.dart +++ b/uni/lib/view/bug_report/bug_report.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:uni/view/bug_report/widgets/form.dart'; -import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; class BugReportPageView extends StatefulWidget { const BugReportPageView({super.key}); @@ -11,7 +11,7 @@ class BugReportPageView extends StatefulWidget { } /// Manages the 'Bugs and sugestions' section of the app. -class BugReportPageViewState extends GeneralPageViewState { +class BugReportPageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { return Container( @@ -22,4 +22,9 @@ class BugReportPageViewState extends GeneralPageViewState { @override Future onRefresh(BuildContext context) async {} + + @override + String? getTitle() { + return null; + } } diff --git a/uni/lib/view/bug_report/widgets/form.dart b/uni/lib/view/bug_report/widgets/form.dart index 18ec82a16..8d957ef94 100644 --- a/uni/lib/view/bug_report/widgets/form.dart +++ b/uni/lib/view/bug_report/widgets/form.dart @@ -4,11 +4,10 @@ import 'package:logger/logger.dart'; import 'package:provider/provider.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/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/app_locale.dart'; import 'package:uni/model/entities/bug_report.dart'; -import 'package:uni/utils/drawer_items.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'; @@ -81,69 +80,52 @@ class BugReportFormState extends State { Widget build(BuildContext context) { return Form( key: _formKey, - child: ListView(children: getFormWidget(context)), - ); - } - - List getFormWidget(BuildContext context) { - return [ - bugReportTitle(context), - bugReportIntro(context), - dropdownBugSelectWidget(context), - FormTextField( - titleController, - Icons.title, - maxLines: 2, - description: S.of(context).title, - labelText: S.of(context).problem_id, - bottomMargin: 30, - ), - FormTextField( - descriptionController, - Icons.description, - maxLines: 30, - description: S.of(context).description, - labelText: S.of(context).bug_description, - bottomMargin: 30, - ), - FormTextField( - emailController, - Icons.mail, - maxLines: 2, - description: S.of(context).contact, - labelText: S.of(context).desired_email, - bottomMargin: 30, - isOptional: true, - formatValidator: (String? value) { - if (value == null || value.isEmpty) { - return null; - } - - return EmailValidator.validate(value) - ? null - : S.of(context).valid_email; - }, - ), - consentBox(context), - submitButton(context), - ]; - } - - /// Returns a widget for the title of the bug report form - Widget bugReportTitle(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Icon(Icons.bug_report, size: 40), + child: ListView( + children: [ + const Padding(padding: EdgeInsets.only(bottom: 10)), PageTitle( - name: S.of(context).nav_title( - DrawerItem.navBugReport.title, - ), - center: false, + name: S.of(context).report_error_suggestion, + pad: false, + ), + const Padding(padding: EdgeInsets.only(bottom: 10)), + bugReportIntro(context), + dropdownBugSelectWidget(context), + FormTextField( + titleController, + Icons.title, + maxLines: 2, + description: S.of(context).title, + labelText: S.of(context).problem_id, + bottomMargin: 30, + ), + FormTextField( + descriptionController, + Icons.description, + maxLines: 30, + description: S.of(context).description, + labelText: S.of(context).bug_description, + bottomMargin: 30, + ), + FormTextField( + emailController, + Icons.mail, + maxLines: 2, + description: S.of(context).contact, + labelText: S.of(context).desired_email, + bottomMargin: 30, + isOptional: true, + formatValidator: (String? value) { + if (value == null || value.isEmpty) { + return null; + } + + return EmailValidator.validate(value) + ? null + : S.of(context).valid_email; + }, ), - const Icon(Icons.bug_report, size: 40), + consentBox(context), + submitButton(context), ], ), ); @@ -261,7 +243,7 @@ class BugReportFormState extends State { setState(() { _isButtonTapped = true; }); - final faculties = await AppSharedPreferences.getUserFaculties(); + final faculties = PreferencesController.getUserFaculties(); final bugReport = BugReport( titleController.text, descriptionController.text, 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 f48205b66..1c32963da 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,16 +1,16 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; -import 'package:uni/model/request_status.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_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/expanded_image_label.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/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; class BusStopNextArrivalsPage extends StatefulWidget { @@ -22,30 +22,90 @@ class BusStopNextArrivalsPage extends StatefulWidget { /// Manages the 'Bus arrivals' section inside the user's personal area class BusStopNextArrivalsPageState - extends GeneralPageViewState { + extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, busProvider) => ListView( + return SingleChildScrollView( + child: Column( children: [ - NextArrivals(busProvider.configuredBusStops, busProvider.status), + Container( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.only(left: 10), + child: const LastUpdateTimeStamp(), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage(), + ), + ), + ), + ], + ), + ), + LazyConsumer>( + builder: getArrivals, + hasContent: (buses) => buses.isNotEmpty, + onNullContent: Column( + children: [ + ImageLabel( + imagePath: 'assets/images/bus.png', + label: S.of(context).no_bus, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 17, + color: Theme.of(context).colorScheme.primary, + ), + ), + Column( + children: [ + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage(), + ), + ), + child: Text(S.of(context).add), + ), + ], + ), + ], + ), + contentLoadingWidget: Container( + padding: const EdgeInsets.all(22), + child: const Center(child: CircularProgressIndicator()), + ), + ), ], ), ); } + Widget getArrivals(BuildContext context, Map buses) { + return NextArrivals(buses); + } + @override Future onRefresh(BuildContext context) async { return Provider.of(context, listen: false) .forceRefresh(context); } + + @override + String? getTitle() => S.of(context).nav_title(NavigationItem.navStops.route); } class NextArrivals extends StatefulWidget { - const NextArrivals(this.buses, this.busStopStatus, {super.key}); - //final Map> trips; + const NextArrivals(this.buses, {super.key}); + final Map buses; - final RequestStatus busStopStatus; @override NextArrivalsState createState() => NextArrivalsState(); @@ -55,165 +115,44 @@ class NextArrivals extends StatefulWidget { 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)), - ); - case RequestStatus.none: - return Container(); - } - } - return DefaultTabController( length: widget.buses.length, - child: contentBuilder(), - ); - } - - /// Returns a list of widgets for a successfull request - - List requestSuccessful(BuildContext context) { - final result = [...getHeader(context)]; - - if (widget.buses.isNotEmpty) { - result.addAll(getContent(context)); - } else { - result - ..add( - ImageLabel( - imagePath: 'assets/images/bus.png', - label: S.of(context).no_bus, - labelTextStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 17, - color: Theme.of(context).colorScheme.primary, - ), - ), - ) - ..add( - Column( - children: [ - ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage(), - ), - ), - child: Text(S.of(context).add), - ), - ], - ), - ); - } - - return result; - } - - /// Returns a list of widgets for a busy request - // TODO(bdmendes): Is this ok? - List requestBusy(BuildContext context) { - return [ - getPageTitle(), - Container( - padding: const EdgeInsets.all(22), - child: const Center(child: CircularProgressIndicator()), - ), - ]; - } - - Container getPageTitle() { - return Container( - padding: const EdgeInsets.only(bottom: 12), - child: PageTitle( - name: S.of(context).nav_title(DrawerItem.navStops.title), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: getContent(context), ), ); } - /// Returns a list of widgets for a failed request - List requestFailed(BuildContext context) { - return [ - ...getHeader(context), - Container( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - 'Não foi possível obter informação', - maxLines: 2, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ]; - } - - List getHeader(BuildContext context) { - return [ - getPageTitle(), - Container( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.only(left: 10), - child: const LastUpdateTimeStamp(), - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage(), - ), - ), - ), - ], - ), - ), - ]; - } - - List getContent(BuildContext context) { + Widget getContent(BuildContext context) { final queryData = MediaQuery.of(context); - return [ - Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(), + return Column( + children: [ + Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(), + ), ), - ), - constraints: const BoxConstraints(maxHeight: 150), - child: Material( - child: TabBar( - isScrollable: true, - tabs: createTabs(queryData), + constraints: const BoxConstraints(maxHeight: 150), + child: Material( + child: TabBar( + isScrollable: true, + tabs: createTabs(queryData), + ), ), ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.only(bottom: 92), - child: TabBarView( - children: getEachBusStopInfo(context), + Expanded( + child: Container( + padding: const EdgeInsets.only(bottom: 92), + child: TabBarView( + children: getEachBusStopInfo(context), + ), ), ), - ), - ]; + ], + ); } List createTabs(MediaQueryData queryData) { @@ -236,7 +175,7 @@ class NextArrivalsState extends State { widget.buses.forEach((stopCode, stopData) { rows.add( - ListView( + Column( children: [ Container( padding: const EdgeInsets.only( 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 1cdbb688f..0661fd001 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 @@ -79,9 +79,6 @@ class BusStopRow extends StatelessWidget { ); } else { for (var i = 0; i < trips.length; i++) { -/* Color color = Theme.of(context).accentColor; - if (i == trips.length - 1) color = Colors.transparent; */ - tripRows.add( Container( padding: const EdgeInsets.all(12), 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 37c6ca6c8..6ea697e78 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/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 { @@ -8,13 +8,14 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { required this.timeRemaining, super.key, }); + final String timeRemaining; @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, busProvider) => - getContent(context, busProvider.timeStamp), + return Consumer( + builder: (context, busProvider, _) => + getContent(context, busProvider.lastUpdateTime ?? DateTime.now()), ); } 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 ecdbfee06..99b7d530f 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/database/app_bus_stop_database.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/bus_stop.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}); @@ -37,10 +36,10 @@ class BusStopSelectionPageState @override Widget getBody(BuildContext context) { final width = MediaQuery.of(context).size.width; - return LazyConsumer( - builder: (context, busProvider) { + return Consumer( + builder: (context, busProvider, _) { final rows = []; - busProvider.configuredBusStops.forEach( + busProvider.state!.forEach( (stopCode, stopData) => rows.add(BusStopSelectionRow(stopCode, stopData)), ); @@ -49,7 +48,6 @@ class BusStopSelectionPageState bottom: 20, ), children: [ - PageTitle(name: S.of(context).configured_buses), Container( padding: const EdgeInsets.all(20), child: Text( @@ -85,4 +83,7 @@ class BusStopSelectionPageState @override Future onRefresh(BuildContext context) async {} + + @override + String? getTitle() => S.of(context).configured_buses; } 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 554c4d8d1..2ac19e288 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 @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; 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/controller/local_storage/database/app_bus_stop_database.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; diff --git a/uni/lib/view/bus_stop_selection/widgets/form.dart b/uni/lib/view/bus_stop_selection/widgets/form.dart index b7ecb6ba7..c6da8923b 100644 --- a/uni/lib/view/bus_stop_selection/widgets/form.dart +++ b/uni/lib/view/bus_stop_selection/widgets/form.dart @@ -7,6 +7,7 @@ import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; class BusesForm extends StatefulWidget { const BusesForm(this.stopCode, this.updateStopCallback, {super.key}); + final String stopCode; final void Function(String, BusStopData) updateStopCallback; @@ -34,7 +35,7 @@ class BusesFormState extends State { }); if (!mounted) return; final currentConfig = Provider.of(context, listen: false) - .configuredBusStops[widget.stopCode]; + .state![widget.stopCode]; if (currentConfig == null) { return; } @@ -70,7 +71,7 @@ class BusesFormState extends State { void updateBusStop() { final currentConfig = Provider.of(context, listen: false) - .configuredBusStops[widget.stopCode]; + .state![widget.stopCode]; final newBuses = {}; for (var i = 0; i < buses.length; i++) { if (busesToAdd[i]) { diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index 1736da7d7..e65739d68 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -4,11 +4,9 @@ import 'package:timelines/timelines.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/providers/lazy/calendar_provider.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/calendar/widgets/calendar_tile.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/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; class CalendarPageView extends StatefulWidget { @@ -18,50 +16,40 @@ class CalendarPageView extends StatefulWidget { State createState() => CalendarPageViewState(); } -class CalendarPageViewState extends GeneralPageViewState { +class CalendarPageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, calendarProvider) => ListView( - children: [ - Container( - padding: const EdgeInsets.only(bottom: 6), - child: PageTitle( - name: S.of(context).nav_title(DrawerItem.navCalendar.title), - ), - ), - 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), - ), - ), - ), - ], + return LazyConsumer>( + builder: getTimeline, + hasContent: (calendar) => calendar.isNotEmpty, + onNullContent: const Center( + child: Text( + 'Nenhum evento encontrado', + style: TextStyle(fontSize: 18), + ), ), ); } Widget getTimeline(BuildContext context, List calendar) { - return FixedTimeline.tileBuilder( - theme: TimelineTheme.of(context).copyWith( - connectorTheme: TimelineTheme.of(context) - .connectorTheme - .copyWith(thickness: 2, color: Theme.of(context).dividerColor), - indicatorTheme: TimelineTheme.of(context) - .indicatorTheme - .copyWith(size: 15, color: Theme.of(context).primaryColor), - ), - builder: TimelineTileBuilder.fromStyle( - contentsAlign: ContentsAlign.alternating, - contentsBuilder: (_, index) => CalendarTile(text: calendar[index].name), - oppositeContentsBuilder: (_, index) => - CalendarTile(text: calendar[index].date, isOpposite: true), - itemCount: calendar.length, + return SingleChildScrollView( + child: FixedTimeline.tileBuilder( + theme: TimelineTheme.of(context).copyWith( + connectorTheme: TimelineTheme.of(context) + .connectorTheme + .copyWith(thickness: 2, color: Theme.of(context).dividerColor), + indicatorTheme: TimelineTheme.of(context) + .indicatorTheme + .copyWith(size: 15, color: Theme.of(context).primaryColor), + ), + builder: TimelineTileBuilder.fromStyle( + contentsAlign: ContentsAlign.alternating, + contentsBuilder: (_, index) => + CalendarTile(text: calendar[index].name), + oppositeContentsBuilder: (_, index) => + CalendarTile(text: calendar[index].date, isOpposite: true), + itemCount: calendar.length, + ), ), ); } @@ -71,4 +59,8 @@ class CalendarPageViewState extends GeneralPageViewState { return Provider.of(context, listen: false) .forceRefresh(context); } + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navCalendar.route); } diff --git a/uni/lib/view/calendar/widgets/calendar_card.dart b/uni/lib/view/calendar/widgets/calendar_card.dart new file mode 100644 index 000000000..e0b2131e8 --- /dev/null +++ b/uni/lib/view/calendar/widgets/calendar_card.dart @@ -0,0 +1,61 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/calendar_event.dart'; +import 'package:uni/model/providers/lazy/calendar_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/calendar/calendar.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; + +class CalendarCard extends GenericCard { + CalendarCard({super.key}); + + const CalendarCard.fromEditingInformation( + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); + + @override + String getTitle(BuildContext context) => + S.of(context).nav_title(NavigationItem.navCalendar.route); + + @override + Future onClick(BuildContext context) => + Navigator.pushNamed(context, '/${NavigationItem.navCalendar.route}'); + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); + } + + @override + Widget buildCardContent(BuildContext context) { + return LazyConsumer>( + builder: (context, events) => CalendarPageViewState() + .getTimeline(context, getFurtherEvents(events)), + hasContent: (calendar) => calendar.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_events, + style: const TextStyle(fontSize: 18), + ), + ), + ); + } + + List getFurtherEvents(List events) { + final sortedEvents = events + .where((element) => element.parsedStartDate != null) + .sorted((a, b) => a.parsedStartDate!.compareTo(b.parsedStartDate!)); + final pinEvent = sortedEvents.firstWhere( + (element) => element.parsedStartDate!.compareTo(DateTime.now()) == 1, + ); + return sortedEvents.sublist( + sortedEvents.indexOf(pinEvent), + sortedEvents.indexOf(pinEvent) + 3, + ); + } +} diff --git a/uni/lib/view/common_widgets/expanded_image_label.dart b/uni/lib/view/common_widgets/expanded_image_label.dart index d2ffc4e07..d7ef8a3d5 100644 --- a/uni/lib/view/common_widgets/expanded_image_label.dart +++ b/uni/lib/view/common_widgets/expanded_image_label.dart @@ -24,12 +24,11 @@ class ImageLabel extends StatelessWidget { height: 300, width: 300, ), - const SizedBox(height: 10), Text( label, style: labelTextStyle, ), - if (sublabel.isNotEmpty) const SizedBox(height: 20), + if (sublabel.isNotEmpty) const SizedBox(height: 10), Text( sublabel, style: sublabelTextStyle, diff --git a/uni/lib/view/common_widgets/faculty_filter.dart b/uni/lib/view/common_widgets/faculty_filter.dart new file mode 100644 index 000000000..caa9055a7 --- /dev/null +++ b/uni/lib/view/common_widgets/faculty_filter.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; + +class FacultyFilter extends StatelessWidget { + const FacultyFilter({ + required this.faculties, + required this.builder, + super.key, + }); + + final List faculties; + final Widget Function(BuildContext context, List authorizedFaculties) + builder; + + @override + Widget build(BuildContext context) { + final authorizedFaculties = PreferencesController.getUserFaculties() + .where( + faculties.contains, + ) + .toList(); + + return authorizedFaculties.isNotEmpty + ? builder(context, authorizedFaculties) + : Container(); + } +} diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index 9bd18de0c..8d966fcda 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/time_utilities.dart'; +import 'package:uni/view/common_widgets/widgets/delete_icon.dart'; +import 'package:uni/view/common_widgets/widgets/move_icon.dart'; /// App default card -abstract class GenericCard extends StatefulWidget { +abstract class GenericCard extends StatelessWidget { GenericCard({Key? key}) : this.customStyle(key: key, editingMode: false, onDelete: () {}); @@ -25,17 +26,16 @@ abstract class GenericCard extends StatefulWidget { this.margin = const EdgeInsets.symmetric(vertical: 10, horizontal: 20), this.hasSmallTitle = false, }); + + static const double borderRadius = 10; + static const double padding = 12; + final EdgeInsetsGeometry margin; final Widget cardAction; final bool hasSmallTitle; final bool editingMode; final void Function()? onDelete; - @override - State createState() { - return GenericCardState(); - } - Widget buildCardContent(BuildContext context); String getTitle(BuildContext context); @@ -65,36 +65,29 @@ abstract class GenericCard extends StatefulWidget { return Container( alignment: Alignment.center, child: Text( - S.of(context).last_refresh_time( - parsedTime.toTimeHourMinString(), - ), + 'última atualização às ${parsedTime.toTimeHourMinString()}', style: Theme.of(context).textTheme.bodySmall, ), ); } -} - -class GenericCardState extends State { - final double borderRadius = 10; - final double padding = 12; @override Widget build(BuildContext context) { return GestureDetector( onTap: () { - if (!widget.editingMode) { - widget.onClick(context); + if (!editingMode) { + onClick(context); } }, child: Card( - margin: widget.margin, + margin: margin, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius), ), child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: const [ + decoration: const BoxDecoration( + boxShadow: [ BoxShadow( color: Color.fromARGB(0x1c, 0, 0, 0), blurRadius: 7, @@ -110,7 +103,8 @@ class GenericCardState extends State { child: Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + borderRadius: + const BorderRadius.all(Radius.circular(borderRadius)), ), width: double.infinity, child: Column( @@ -126,8 +120,8 @@ class GenericCardState extends State { padding: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.only(top: 15, bottom: 10), child: Text( - widget.getTitle(context), - style: (widget.hasSmallTitle + getTitle(context), + style: (hasSmallTitle ? Theme.of(context).textTheme.titleLarge! : Theme.of(context) .textTheme @@ -138,23 +132,23 @@ class GenericCardState extends State { ), ), ), - widget.cardAction, - if (widget.editingMode) + cardAction, + if (editingMode) Container( alignment: Alignment.center, margin: const EdgeInsets.only(top: 8), - child: getMoveIcon(context), + child: const MoveIcon(), ), - if (widget.editingMode) getDeleteIcon(context), + if (editingMode) DeleteIcon(onDelete: onDelete), ], ), Container( - padding: EdgeInsets.only( + padding: const EdgeInsets.only( left: padding, right: padding, bottom: padding, ), - child: widget.buildCardContent(context), + child: buildCardContent(context), ), ], ), @@ -164,27 +158,4 @@ class GenericCardState extends State { ), ); } - - Widget getDeleteIcon(BuildContext context) { - return Flexible( - child: Container( - alignment: Alignment.centerRight, - height: 32, - child: IconButton( - iconSize: 22, - icon: const Icon(Icons.delete), - tooltip: 'Remover', - onPressed: widget.onDelete, - ), - ), - ); - } - - Widget getMoveIcon(BuildContext context) { - return Icon( - Icons.drag_handle_rounded, - color: Colors.grey.shade500, - size: 22, - ); - } } diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index 1e9e31e97..4e70543fe 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; -import 'package:uni/view/lazy_consumer.dart'; -class LastUpdateTimeStamp +class LastUpdateTimeStamp> extends StatefulWidget { const LastUpdateTimeStamp({super.key}); @@ -15,8 +15,8 @@ class LastUpdateTimeStamp } } -class _LastUpdateTimeStampState - extends State { +class _LastUpdateTimeStampState> + extends State { DateTime currentTime = DateTime.now(); @override @@ -37,8 +37,8 @@ class _LastUpdateTimeStampState @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, provider) => Container( + return Consumer( + builder: (context, provider, _) => Container( padding: const EdgeInsets.only(top: 8, bottom: 10), child: provider.lastUpdateTime != null ? _getContent(context, provider.lastUpdateTime!) diff --git a/uni/lib/view/common_widgets/page_title.dart b/uni/lib/view/common_widgets/page_title.dart index af5bbeccd..ccc46a440 100644 --- a/uni/lib/view/common_widgets/page_title.dart +++ b/uni/lib/view/common_widgets/page_title.dart @@ -21,8 +21,8 @@ class PageTitle extends StatelessWidget { ), ); return Container( - padding: pad ? const EdgeInsets.fromLTRB(20, 20, 20, 10) : null, - alignment: center ? Alignment.center : null, + padding: pad ? const EdgeInsets.fromLTRB(20, 30, 20, 10) : null, + alignment: center ? Alignment.center : Alignment.centerLeft, child: title, ); } 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 0314f2eb4..5ea94a8ff 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -2,21 +2,23 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:uni/generated/l10n.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'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/profile_button.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/refresh_state.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart'; /// Page with a hamburger menu and the user profile picture abstract class GeneralPageViewState extends State { - final double borderMargin = 18; bool _loadedOnce = false; bool _loading = true; + bool _connected = true; Future onRefresh(BuildContext context); @@ -36,8 +38,14 @@ abstract class GeneralPageViewState extends State { try { await onLoad(context); } catch (e, stackTrace) { - Logger().e('Failed to load page info: $e\n$stackTrace'); - await Sentry.captureException(e, stackTrace: stackTrace); + if (e is SocketException) { + setState(() { + _connected = false; + }); + } else { + Logger().e('Failed to load page info: $e\n$stackTrace'); + await Sentry.captureException(e, stackTrace: stackTrace); + } } if (mounted) { @@ -47,6 +55,27 @@ abstract class GeneralPageViewState extends State { } }); + if (!_connected) { + return getScaffold( + context, + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 35), + child: ImageLabel( + imagePath: 'assets/images/no_wifi.png', + label: S.of(context).no_internet, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), + sublabel: S.of(context).check_internet, + ), + ), + ), + ); + } + return getScaffold( context, _loading @@ -64,133 +93,42 @@ abstract class GeneralPageViewState extends State { ); } - Widget getBody(BuildContext context) { - return Container(); - } - - Future buildProfileDecorationImage( - BuildContext context, { - bool forceRetrieval = false, - }) async { - final sessionProvider = - Provider.of(context, listen: false); - await sessionProvider.ensureInitializedFromStorage(); - final profilePictureFile = - await ProfileProvider.fetchOrGetCachedProfilePicture( - sessionProvider.session, - forceRetrieval: forceRetrieval, - ); - return getProfileDecorationImage(profilePictureFile); - } - - /// Returns the current user image. - /// - /// If the image is not found / doesn't exist returns a generic placeholder. - DecorationImage getProfileDecorationImage(File? profilePicture) { - const fallbackPicture = AssetImage('assets/images/profile_placeholder.png'); - final image = - profilePicture == null ? fallbackPicture : FileImage(profilePicture); + String? getTitle(); - final result = - DecorationImage(fit: BoxFit.cover, image: image as ImageProvider); - return result; - } + Widget getBody(BuildContext context); Widget refreshState(BuildContext context, Widget child) { return RefreshIndicator( key: GlobalKey(), onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture( - Provider.of(context, listen: false).session, + Provider.of(context, listen: false).state!, forceRetrieval: true, ).then((value) => onRefresh(context)), - child: child, + child: Builder( + builder: (context) => GestureDetector( + onHorizontalDragEnd: (dragDetails) { + if (dragDetails.primaryVelocity! > 2) { + Scaffold.of(context).openDrawer(); + } + }, + child: child, + ), + ), ); } Widget getScaffold(BuildContext context, Widget body) { return Scaffold( - appBar: buildAppBar(context), - drawer: AppNavigationDrawer(parentContext: context), - body: refreshState(context, body), - ); - } - - /// Builds the upper bar of the app. - /// - /// This method returns an instance of `AppBar` containing the app's logo, - /// an option button and a button with the user's picture. - AppBar buildAppBar(BuildContext context) { - final queryData = MediaQuery.of(context); - - return AppBar( - bottom: PreferredSize( - preferredSize: Size.zero, - child: Container( - color: Theme.of(context).dividerColor, - margin: EdgeInsets.only(left: borderMargin, right: borderMargin), - height: 1.5, - ), - ), - elevation: 0, - iconTheme: Theme.of(context).iconTheme, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - titleSpacing: 0, - title: ButtonTheme( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: const RoundedRectangleBorder(), - child: TextButton( - onPressed: () { - final currentRouteName = ModalRoute.of(context)!.settings.name; - if (currentRouteName != DrawerItem.navPersonalArea.title) { - Navigator.pushNamed( - context, - '/${DrawerItem.navPersonalArea.title}', - ); - } - }, - child: SvgPicture.asset( - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, - BlendMode.srcIn, - ), - 'assets/images/logo_dark.svg', - height: queryData.size.height / 25, - ), - ), - ), - actions: [ - getTopRightButton(context), - ], + bottomNavigationBar: const AppBottomNavbar(), + appBar: getTopNavbar(context), + body: RefreshState(onRefresh: onRefresh, child: body), ); } - // Gets a round shaped button with the photo of the current user. - Widget getTopRightButton(BuildContext context) { - return FutureBuilder( - future: buildProfileDecorationImage(context), - builder: ( - BuildContext context, - AsyncSnapshot decorationImage, - ) { - return TextButton( - onPressed: () => { - Navigator.push( - context, - MaterialPageRoute( - builder: (__) => const ProfilePageView(), - ), - ), - }, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: decorationImage.data, - ), - ), - ); - }, + AppTopNavbar? getTopNavbar(BuildContext context) { + return AppTopNavbar( + title: this.getTitle(), + rightButton: const ProfileButton(), ); } } diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart new file mode 100644 index 000000000..ab00be2d4 --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:uni/utils/navbar_items.dart'; + +class AppBottomNavbar extends StatelessWidget { + const AppBottomNavbar({super.key}); + + String? _getCurrentRoute(BuildContext context) => + ModalRoute.of(context)!.settings.name?.substring(1); + + int _getCurrentIndex(BuildContext context) { + final currentRoute = _getCurrentRoute(context); + if (_getCurrentRoute(context) == null) { + return -1; + } + + for (final item in NavbarItem.values) { + if (item.route == currentRoute) { + return item.index; + } + } + + return -1; + } + + void _onItemTapped(BuildContext context, int index) { + final prev = _getCurrentRoute(context); + final item = NavbarItem.values[index]; + final key = item.route; + + if (prev != key) { + Navigator.pushNamed( + context, + '/$key', + ); + } + } + + @override + Widget build(BuildContext context) { + final currentIndex = _getCurrentIndex(context); + final navbarItems = []; + for (var index = 0; index < NavbarItem.values.length; index++) { + final item = NavbarItem.values[index]; + navbarItems.insert( + index, + index == currentIndex + ? item.toSelectedBottomNavigationBarItem(context) + : item.toUnselectedBottomNavigationBarItem(context), + ); + } + + return Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: BottomNavigationBar( + items: navbarItems, + onTap: (int index) => _onItemTapped(context, index), + currentIndex: currentIndex == -1 ? 0 : currentIndex, + type: BottomNavigationBarType.fixed, + iconSize: 32, + selectedItemColor: currentIndex == -1 + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.secondary, + unselectedItemColor: Theme.of(context).colorScheme.onSurface, + selectedFontSize: 0, + unselectedFontSize: 0, + showSelectedLabels: false, + showUnselectedLabels: false, + ), + ); + } +} diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/drawer_navigation_option.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/drawer_navigation_option.dart new file mode 100644 index 000000000..b7092271d --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/drawer_navigation_option.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:uni/utils/navigation_items.dart'; + +class DrawerNavigationOption extends StatelessWidget { + const DrawerNavigationOption({required this.item, super.key}); + + final NavigationItem item; + + String getCurrentRoute(BuildContext context) => + ModalRoute.of(context)!.settings.name == null + ? NavigationItem.values.toList()[0].route + : ModalRoute.of(context)!.settings.name!.substring(1); + + void onSelectPage(String key, BuildContext context) { + final prev = getCurrentRoute(context); + + Navigator.of(context).pop(); + + if (prev != key) { + Navigator.pushNamed(context, '/$key'); + } + } + + BoxDecoration? _getSelectionDecoration(String name, BuildContext context) { + return (name == getCurrentRoute(context)) + ? BoxDecoration( + border: Border( + left: BorderSide( + color: Theme.of(context).primaryColor, + width: 3, + ), + ), + color: Theme.of(context).dividerColor, + ) + : null; + } + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: + _getSelectionDecoration(item.route, context) ?? const BoxDecoration(), + child: ListTile( + title: Container( + padding: const EdgeInsets.only(bottom: 3, left: 20), + child: Text( + item.route, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.normal, + ), + ), + ), + dense: true, + contentPadding: EdgeInsets.zero, + selected: item.route == getCurrentRoute(context), + onTap: () => onSelectPage(item.route, 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 deleted file mode 100644 index 18572148d..000000000 --- a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/generated/l10n.dart'; -import 'package:uni/model/providers/startup/session_provider.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/locale_notifier.dart'; -import 'package:uni/view/theme_notifier.dart'; - -class AppNavigationDrawer extends StatefulWidget { - const AppNavigationDrawer({required this.parentContext, super.key}); - final BuildContext parentContext; - - @override - State createState() { - return AppNavigationDrawerState(); - } -} - -class AppNavigationDrawerState extends State { - AppNavigationDrawerState(); - - Map drawerItems = {}; - - @override - void initState() { - super.initState(); - - drawerItems = {}; - for (final element in DrawerItem.values) { - drawerItems[element] = _onSelectPage; - } - } - - // Callback Functions - String getCurrentRoute() => - ModalRoute.of(widget.parentContext)!.settings.name == null - ? drawerItems.keys.toList()[0].title - : ModalRoute.of(widget.parentContext)!.settings.name!.substring(1); - - void _onSelectPage(String key) { - final prev = getCurrentRoute(); - - Navigator.of(context).pop(); - - if (prev != key) { - Navigator.pushNamed(context, '/$key'); - } - } - - void _onLogOut(String key) { - Navigator.of(context) - .pushNamedAndRemoveUntil('/$key', (Route route) => false); - } - - // End of Callback Functions - - BoxDecoration? _getSelectionDecoration(String name) { - return (name == getCurrentRoute()) - ? BoxDecoration( - border: Border( - left: BorderSide( - color: Theme.of(context).primaryColor, - width: 3, - ), - ), - color: Theme.of(context).dividerColor, - ) - : null; - } - - Widget createLogoutBtn() { - final logOutText = DrawerItem.navLogOut.title; - return TextButton( - onPressed: () => _onLogOut(logOutText), - style: TextButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 5), - ), - child: Container( - padding: const EdgeInsets.all(15), - child: Text( - S.of(context).logout, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).primaryColor), - ), - ), - ); - } - - Widget createLocaleBtn() { - return Consumer( - builder: (context, localeNotifier, _) { - return TextButton( - onPressed: () => localeNotifier.setNextLocale(), - style: TextButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 5), - ), - child: Container( - padding: const EdgeInsets.all(15), - child: Text( - localeNotifier.getLocale().localeCode.languageCode.toUpperCase(), - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).primaryColor), - ), - ), - ); - }, - ); - } - - Widget createThemeSwitchBtn() { - Icon getThemeIcon(ThemeMode theme) { - switch (theme) { - case ThemeMode.light: - return const Icon(Icons.wb_sunny); - case ThemeMode.dark: - return const Icon(Icons.nightlight_round); - case ThemeMode.system: - return const Icon(Icons.brightness_6); - } - } - - return Consumer( - builder: (context, themeNotifier, _) { - return IconButton( - icon: getThemeIcon(themeNotifier.getTheme()), - onPressed: themeNotifier.setNextTheme, - ); - }, - ); - } - - Widget createDrawerNavigationOption(DrawerItem d) { - return DecoratedBox( - decoration: _getSelectionDecoration(d.title) ?? const BoxDecoration(), - child: ListTile( - title: Container( - padding: const EdgeInsets.only(bottom: 3, left: 20), - child: Text( - S.of(context).nav_title(d.title), - style: TextStyle( - fontSize: 18, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.normal, - ), - ), - ), - dense: true, - contentPadding: EdgeInsets.zero, - selected: d.title == getCurrentRoute(), - onTap: () => drawerItems[d]!(d.title), - ), - ); - } - - @override - Widget build(BuildContext context) { - final drawerOptions = []; - final userSession = Provider.of(context).session; - - for (final key in drawerItems.keys) { - if (key.isVisible(userSession.faculties)) { - drawerOptions.add(createDrawerNavigationOption(key)); - } - } - - return Drawer( - child: Column( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(top: 55), - child: ListView( - children: drawerOptions, - ), - ), - ), - Row( - children: [ - Expanded( - child: Align( - child: createLogoutBtn(), - ), - ), - Align( - alignment: Alignment.centerRight, - child: createLocaleBtn(), - ), - createThemeSwitchBtn(), - ], - ), - ], - ), - ); - } -} diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/profile_button.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/profile_button.dart new file mode 100644 index 000000000..4c5c6a271 --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/profile_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/widgets/profile_image.dart'; + +class ProfileButton extends StatelessWidget { + const ProfileButton({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.all(5), + shape: const CircleBorder(), + ), + onPressed: () => { + Navigator.pushNamed( + context, + '/${NavigationItem.navProfile.route}', + ), + }, + icon: const ProfileImage(radius: 20), + ); + } +} diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/refresh_state.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/refresh_state.dart new file mode 100644 index 000000000..199579429 --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/refresh_state.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; + +class RefreshState extends StatelessWidget { + const RefreshState({required this.onRefresh, required this.child, super.key}); + + final Future Function(BuildContext) onRefresh; + final Widget child; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + key: GlobalKey(), + onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture( + Provider.of(context, listen: false).state!, + forceRetrieval: true, + ).then((value) => onRefresh(context)), + child: child, + ); + } +} diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/theme_switch_button.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/theme_switch_button.dart new file mode 100644 index 000000000..2b761e9c4 --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/theme_switch_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/view/theme_notifier.dart'; + +class ThemeSwitchButton extends StatelessWidget { + const ThemeSwitchButton({super.key}); + + Icon getThemeIcon(ThemeMode theme) { + switch (theme) { + case ThemeMode.light: + return const Icon(Icons.wb_sunny); + case ThemeMode.dark: + return const Icon(Icons.nightlight_round); + case ThemeMode.system: + return const Icon(Icons.brightness_6); + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, themeNotifier, _) { + return IconButton( + icon: getThemeIcon(themeNotifier.getTheme()), + onPressed: themeNotifier.setNextTheme, + ); + }, + ); + } +} diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart new file mode 100644 index 000000000..fb1bbab51 --- /dev/null +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:uni/view/common_widgets/page_title.dart'; + +class AppTopNavbar extends StatelessWidget implements PreferredSizeWidget { + const AppTopNavbar({ + this.title, + this.rightButton, + this.leftButton, + super.key, + }); + + static const double borderMargin = 18; + + final String? title; + final Widget? rightButton; + final Widget? leftButton; + + Widget _createTopWidgets(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(leftButton == null ? 20 : 12, 0, 20, 0), + child: Row( + children: [ + if (leftButton != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: leftButton, + ), + Expanded( + child: PageTitle( + name: title ?? '', + pad: false, + center: false, + ), + ), + if (rightButton != null) + Padding( + padding: const EdgeInsets.only(left: 8), + child: rightButton, + ), + ], + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + AppBar build(BuildContext context) { + return AppBar( + automaticallyImplyLeading: false, + elevation: 0, + iconTheme: Theme.of(context).iconTheme, + backgroundColor: Theme.of(context).colorScheme.background, + shadowColor: Theme.of(context).dividerColor, + surfaceTintColor: Theme.of(context).colorScheme.onSecondary, + titleSpacing: 0, + title: _createTopWidgets(context), + ); + } +} diff --git a/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart b/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart index 473fb3299..b06dc6fce 100644 --- a/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart +++ b/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart @@ -1,5 +1,9 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/refresh_state.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart'; /// Page with a back button on top abstract class SecondaryPageViewState @@ -7,8 +11,26 @@ abstract class SecondaryPageViewState @override Scaffold getScaffold(BuildContext context, Widget body) { return Scaffold( - appBar: buildAppBar(context), - body: refreshState(context, body), + appBar: getTopNavbar(context), + bottomNavigationBar: const AppBottomNavbar(), + body: RefreshState(onRefresh: onRefresh, child: body), + ); + } + + @override + String? getTitle(); + + Widget? getTopRightButton(BuildContext context) { + return null; + } + + @override + @nonVirtual + AppTopNavbar? getTopNavbar(BuildContext context) { + return AppTopNavbar( + title: getTitle(), + leftButton: const BackButton(), + rightButton: getTopRightButton(context), ); } } diff --git a/uni/lib/view/common_widgets/pulse_animation.dart b/uni/lib/view/common_widgets/pulse_animation.dart new file mode 100644 index 000000000..45476c9b2 --- /dev/null +++ b/uni/lib/view/common_widgets/pulse_animation.dart @@ -0,0 +1,25 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class PulseAnimation extends StatelessWidget { + const PulseAnimation({ + required this.child, + required this.controller, + super.key, + }); + final Widget child; + final AnimationController controller; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, _) { + return Opacity( + opacity: 1 - 0.5 * sin(controller.value * pi), + child: child, + ); + }, + ); + } +} diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart deleted file mode 100644 index 5efd0dfe6..000000000 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:uni/generated/l10n.dart'; -import 'package:uni/model/request_status.dart'; -import 'package:uni/utils/drawer_items.dart'; - -/// Wraps content given its fetch data from the redux store, -/// hydrating the component, displaying an empty message, -/// a connection error or a loading circular effect as appropriate -class RequestDependentWidgetBuilder extends StatelessWidget { - const RequestDependentWidgetBuilder({ - required this.status, - required this.builder, - required this.hasContentPredicate, - required this.onNullContent, - super.key, - this.contentLoadingWidget, - }); - - final RequestStatus status; - final Widget Function() builder; - final Widget? contentLoadingWidget; - final bool hasContentPredicate; - final Widget onNullContent; - - @override - Widget build(BuildContext context) { - 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) { - return const Center( - heightFactor: 3, - child: CircularProgressIndicator(), - ); - } - - if (connectivitySnapshot.data == ConnectivityResult.none) { - return Center( - heightFactor: 3, - child: Text( - S.of(context).check_internet, - style: Theme.of(context).textTheme.titleMedium, - ), - ); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 15, bottom: 10), - child: Center( - child: Text( - S.of(context).load_error, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - OutlinedButton( - onPressed: () => Navigator.pushNamed( - context, - '/${DrawerItem.navBugReport.title}', - ), - child: Text(S.of(context).report_error), - ), - ], - ); - }, - ); - } -} diff --git a/uni/lib/view/common_widgets/widgets/delete_icon.dart b/uni/lib/view/common_widgets/widgets/delete_icon.dart new file mode 100644 index 000000000..e2b6c8c8a --- /dev/null +++ b/uni/lib/view/common_widgets/widgets/delete_icon.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class DeleteIcon extends StatelessWidget { + const DeleteIcon({this.onDelete, super.key}); + + final void Function()? onDelete; + + @override + Widget build(BuildContext context) { + return Flexible( + child: Container( + alignment: Alignment.centerRight, + height: 32, + child: IconButton( + iconSize: 22, + icon: const Icon(Icons.delete), + tooltip: 'Remover', + onPressed: onDelete, + ), + ), + ); + } +} diff --git a/uni/lib/view/common_widgets/widgets/move_icon.dart b/uni/lib/view/common_widgets/widgets/move_icon.dart new file mode 100644 index 000000000..92b28286d --- /dev/null +++ b/uni/lib/view/common_widgets/widgets/move_icon.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class MoveIcon extends StatelessWidget { + const MoveIcon({super.key}); + + @override + Widget build(BuildContext context) { + return Icon( + Icons.drag_handle_rounded, + color: Colors.grey.shade500, + size: 22, + ); + } +} diff --git a/uni/lib/view/common_widgets/widgets/profile_image.dart b/uni/lib/view/common_widgets/widgets/profile_image.dart new file mode 100644 index 000000000..35b0d39f6 --- /dev/null +++ b/uni/lib/view/common_widgets/widgets/profile_image.dart @@ -0,0 +1,59 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; + +class ProfileImage extends StatelessWidget { + const ProfileImage({ + required this.radius, + super.key, + }); + + final double radius; + + Future buildProfileDecorationImage( + BuildContext context, + ) async { + final sessionProvider = + Provider.of(context, listen: false); + await sessionProvider.ensureInitialized(context); + final profilePictureFile = + await ProfileProvider.fetchOrGetCachedProfilePicture( + sessionProvider.state!, + ); + return getProfileDecorationImage(profilePictureFile); + } + + /// Returns the current user image. + /// + /// If the image is not found / doesn't exist returns null. + DecorationImage? getProfileDecorationImage(File? profilePicture) { + final image = profilePicture != null + ? FileImage(profilePicture) as ImageProvider + : const AssetImage('assets/images/profile_placeholder.png'); + + return DecorationImage( + fit: BoxFit.cover, + image: image, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: buildProfileDecorationImage(context), + builder: ( + BuildContext context, + AsyncSnapshot decorationImage, + ) { + return CircleAvatar( + radius: radius, + foregroundImage: decorationImage.data?.image, + backgroundColor: Colors.transparent, + ); + }, + ); + } +} diff --git a/uni/lib/view/common_widgets/widgets/request_failed_message.dart b/uni/lib/view/common_widgets/widgets/request_failed_message.dart new file mode 100644 index 000000000..6238d966b --- /dev/null +++ b/uni/lib/view/common_widgets/widgets/request_failed_message.dart @@ -0,0 +1,59 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/bug_report/bug_report.dart'; + +class RequestFailedMessage extends StatelessWidget { + const RequestFailedMessage({super.key}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Connectivity().checkConnectivity(), + builder: ( + BuildContext context, + AsyncSnapshot connectivitySnapshot, + ) { + if (!connectivitySnapshot.hasData) { + return const Center( + heightFactor: 3, + child: CircularProgressIndicator(), + ); + } + + if (connectivitySnapshot.data == ConnectivityResult.none) { + return Center( + heightFactor: 3, + child: Text( + S.of(context).check_internet, + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 10), + child: Center( + child: Text( + S.of(context).load_error, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + OutlinedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BugReportPageView(), + ), + ), + child: Text(S.of(context).report_error), + ), + ], + ); + }, + ); + } +} 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 aa38ec088..80c5ba0e3 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -4,9 +4,11 @@ import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/providers/lazy/course_units_info_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/utils/navigation_items.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/course_unit_info/widgets/course_unit_classes.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_files.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_sheet.dart'; class CourseUnitDetailPageView extends StatefulWidget { @@ -25,7 +27,7 @@ class CourseUnitDetailPageViewState Future loadInfo({required bool force}) async { final courseUnitsProvider = Provider.of(context, listen: false); - final session = context.read().session; + final session = context.read().state!; final courseUnitSheet = courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; @@ -36,6 +38,15 @@ class CourseUnitDetailPageViewState ); } + final courseUnitFiles = + courseUnitsProvider.courseUnitsFiles[widget.courseUnit]; + if (courseUnitFiles == null || force) { + await courseUnitsProvider.fetchCourseUnitFiles( + widget.courseUnit, + session, + ); + } + final courseUnitClasses = courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; if (courseUnitClasses == null || force) { @@ -59,7 +70,7 @@ class CourseUnitDetailPageViewState @override Widget getBody(BuildContext context) { return DefaultTabController( - length: 2, + length: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -71,6 +82,9 @@ class CourseUnitDetailPageViewState tabs: [ Tab(text: S.of(context).course_info), Tab(text: S.of(context).course_class), + Tab( + text: S.of(context).files, + ), ], ), Expanded( @@ -80,6 +94,7 @@ class CourseUnitDetailPageViewState children: [ _courseUnitSheetView(context), _courseUnitClassesView(context), + _courseUnitFilesView(context), ], ), ), @@ -106,6 +121,23 @@ class CourseUnitDetailPageViewState return CourseUnitSheetView(sheet); } + Widget _courseUnitFilesView(BuildContext context) { + final files = context + .watch() + .courseUnitsFiles[widget.courseUnit]; + + if (files == null || files.isEmpty) { + return Center( + child: Text( + S.of(context).no_files_found, + textAlign: TextAlign.center, + ), + ); + } + + return CourseUnitFilesView(files); + } + Widget _courseUnitClassesView(BuildContext context) { final classes = context .read() @@ -122,4 +154,8 @@ class CourseUnitDetailPageViewState return CourseUnitClassesView(classes); } + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navCourseUnits.route); } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart index afd997326..5c5822c06 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -7,11 +7,12 @@ import 'package:uni/view/course_unit_info/widgets/course_unit_student_row.dart'; class CourseUnitClassesView extends StatelessWidget { const CourseUnitClassesView(this.classes, {super.key}); + final List classes; @override Widget build(BuildContext context) { - final session = context.read().session; + final session = context.read().state!; final cards = []; for (final courseUnitClass in classes) { final isMyClass = courseUnitClass.students diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart new file mode 100644 index 000000000..abd1ea77f --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_files_row.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; + +class CourseUnitFilesView extends StatelessWidget { + const CourseUnitFilesView(this.files, {super.key}); + final List files; + + @override + Widget build(BuildContext context) { + final cards = files + .where((element) => element.files.isNotEmpty) + .map((e) => _buildCard(e.folderName, e.files)) + .toList(); + + return cards.isEmpty + ? Center( + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Text(S.of(context).no_files_found), + ), + ) + : Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView( + children: cards, + ), + ); + } + + CourseUnitInfoCard _buildCard(String folder, List files) => + CourseUnitInfoCard( + folder, + Column( + children: files.map(CourseUnitFilesRow.new).toList(), + ), + ); +} diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart new file mode 100644 index 000000000..88c26c6dc --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -0,0 +1,124 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:open_file_plus/open_file_plus.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/file_offline_storage.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/view/common_widgets/pulse_animation.dart'; +import 'package:uni/view/common_widgets/toast_message.dart'; + +class CourseUnitFilesRow extends StatefulWidget { + const CourseUnitFilesRow(this.file, {super.key}); + + final CourseUnitFile file; + + @override + State createState() { + return CourseUnitFilesRowState(); + } +} + +class CourseUnitFilesRowState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + children: [ + const SizedBox(width: 8), + const Icon(Icons.picture_as_pdf), + const SizedBox(width: 1), + Expanded( + child: GestureDetector( + onTap: () { + _controller + ..reset() + ..repeat(reverse: true); + openFile(context, widget.file); + }, + child: Container( + padding: const EdgeInsets.only(left: 10), + child: PulseAnimation( + controller: _controller, + child: Text( + widget.file.name + .substring(0, widget.file.name.indexOf('_')), + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Future openFile(BuildContext context, CourseUnitFile unitFile) async { + final session = context.read().state; + + final result = await loadFileFromStorageOrRetrieveNew( + unitFile.name, + unitFile.url, + session, + headers: {'pct_id': unitFile.fileCode}, + ); + + if (result?.path != null) { + final resultType = await OpenFile.open(result!.path); + if (context.mounted) handleFileOpening(resultType.type, context); + } else { + if (context.mounted) { + await ToastMessage.error(context, S.of(context).download_error); + } + } + + _controller.reset(); + } + + void handleFileOpening(ResultType resultType, BuildContext context) { + switch (resultType) { + case ResultType.done: + ToastMessage.success( + context, + S.of(context).successful_open, + ); + case ResultType.error: + ToastMessage.error( + context, + S.of(context).open_error, + ); + case ResultType.noAppToOpen: + ToastMessage.warning( + context, + S.of(context).no_app, + ); + case ResultType.permissionDenied: + ToastMessage.warning(context, S.of(context).permission_denied); + case ResultType.fileNotFound: + ToastMessage.error(context, S.of(context).download_error); + } + } +} diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 26c1b1715..8e9383405 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -3,12 +3,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; +import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; -import 'package:uni/model/request_status.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; -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/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/course_units/widgets/course_unit_card.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -24,15 +22,15 @@ class CourseUnitsPageView extends StatefulWidget { } class CourseUnitsPageViewState - extends GeneralPageViewState { + extends SecondaryPageViewState { String? selectedSchoolYear; String? selectedSemester; @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, profileProvider) { - final courseUnits = profileProvider.profile.courseUnits; + return LazyConsumer( + builder: (context, profile) { + final courseUnits = profile.courseUnits; var availableYears = []; var availableSemesters = []; @@ -52,19 +50,31 @@ class CourseUnitsPageViewState } } - return _getPageView( - courseUnits, - profileProvider.status, - availableYears, - availableSemesters, + return Column( + children: [ + _getFilters(availableYears, availableSemesters), + _getPageView(courseUnits, availableYears, availableSemesters), + ], ); }, + hasContent: (Profile profile) => profile.courseUnits.isNotEmpty, + onNullContent: Column( + children: [ + _getFilters([], []), + Center( + heightFactor: 10, + child: Text( + S.of(context).no_selected_courses, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ], + ), ); } Widget _getPageView( List courseUnits, - RequestStatus requestStatus, List availableYears, List availableSemesters, ) { @@ -80,36 +90,16 @@ class CourseUnitsPageViewState element.semesterCode == selectedSemester, ) .toList(); - return Column( - children: [ - _getPageTitleAndFilters(availableYears, availableSemesters), - RequestDependentWidgetBuilder( - status: requestStatus, - builder: () => - _generateCourseUnitsCards(filteredCourseUnits, context), - hasContentPredicate: courseUnits.isNotEmpty, - onNullContent: Center( - heightFactor: 10, - child: Text( - S.of(context).no_selected_courses, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - ], - ); + return _generateCourseUnitsCards(filteredCourseUnits, context); } - Widget _getPageTitleAndFilters( + Widget _getFilters( List availableYears, List availableSemesters, ) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - PageTitle( - name: S.of(context).nav_title(DrawerItem.navCourseUnits.title), - ), const Spacer(), DropdownButtonHideUnderline( child: DropdownButton( @@ -230,4 +220,8 @@ class CourseUnitsPageViewState return Provider.of(context, listen: false) .forceRefresh(context); } + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navCourseUnits.route); } 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 73ee045e8..f88d31429 100644 --- a/uni/lib/view/course_units/widgets/course_unit_card.dart +++ b/uni/lib/view/course_units/widgets/course_unit_card.dart @@ -20,7 +20,8 @@ class CourseUnitCard extends GenericCard { padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), child: Row( children: [ - Text("${courseUnit.ects.toString().replaceAll('.0', '')} ECTS"), + if (courseUnit.ects != null) + Text("${courseUnit.ects.toString().replaceAll('.0', '')} ECTS"), const Spacer(), Text(courseUnit.grade ?? '-'), ], diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 036b254ad..46b019d8f 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/utils/date_time_formatter.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/expanded_image_label.dart'; -import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/common_widgets/row_container.dart'; -import 'package:uni/view/exams/widgets/day_title.dart'; -import 'package:uni/view/exams/widgets/exam_page_title.dart'; +import 'package:uni/view/exams/widgets/exam_filter_button.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locale_notifier.dart'; @@ -19,55 +21,59 @@ class ExamsPageView extends StatefulWidget { State createState() => ExamsPageViewState(); } -/// Tracks the state of `ExamsLists`. -class ExamsPageViewState extends GeneralPageViewState { - final double borderRadius = 10; +class ExamsPageViewState extends SecondaryPageViewState { + List hiddenExams = PreferencesController.getHiddenExams(); + Map filteredExamTypes = + PreferencesController.getFilteredExams(); @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, examProvider) { - return ListView( - children: [ - Column( - children: - createExamsColumn(context, examProvider.getFilteredExams()), + return ListView( + children: [ + LazyConsumer>( + builder: (context, exams) { + return Column( + children: createExamsColumn( + context, + exams + .where( + (exam) => + filteredExamTypes[Exam.getExamTypeLong(exam.type)] ?? + true, + ) + .toList(), + ), + ); + }, + hasContent: (exams) => exams.isNotEmpty, + onNullContent: Center( + heightFactor: 1.2, + child: ImageLabel( + imagePath: 'assets/images/vacation.png', + label: S.of(context).no_exams_label, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), + sublabel: S.of(context).no_exams, + sublabelTextStyle: const TextStyle(fontSize: 15), ), - ], - ); - }, + ), + ), + ], ); } /// Creates a column with all the user's exams. List createExamsColumn(BuildContext context, List exams) { - final columns = [const ExamPageTitle()]; - - if (exams.isEmpty) { - columns.add( - Center( - heightFactor: 1.2, - child: ImageLabel( - imagePath: 'assets/images/vacation.png', - label: S.of(context).no_exams_label, - labelTextStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Theme.of(context).colorScheme.primary, - ), - sublabel: S.of(context).no_exams, - sublabelTextStyle: const TextStyle(fontSize: 15), - ), - ), - ); - return columns; - } - if (exams.length == 1) { - columns.add(createExamCard(context, [exams[0]])); - return columns; + return [ + createExamCard(context, [exams[0]]), + ]; } + final columns = []; final currentDayExams = []; for (var i = 0; i < exams.length; i++) { @@ -95,6 +101,7 @@ class ExamsPageViewState extends GeneralPageViewState { currentDayExams.clear(); } } + return columns; } @@ -111,10 +118,14 @@ class ExamsPageViewState extends GeneralPageViewState { Widget createExamsCards(BuildContext context, List exams) { final locale = Provider.of(context).getLocale(); final examCards = [ - DayTitle( - day: exams[0].begin.day.toString(), - weekDay: exams[0].weekDay(locale), - month: exams[0].month(locale), + Container( + padding: const EdgeInsets.only(top: 15, bottom: 3), + alignment: Alignment.center, + child: Text( + '${exams.first.weekDay(locale)}, ' + '${exams.first.begin.formattedDate(locale)}', + style: Theme.of(context).textTheme.titleLarge, + ), ), ]; for (var i = 0; i < exams.length; i++) { @@ -124,8 +135,7 @@ class ExamsPageViewState extends GeneralPageViewState { } Widget createExamContext(BuildContext context, Exam exam) { - final isHidden = - Provider.of(context).hiddenExams.contains(exam.id); + final isHidden = hiddenExams.contains(exam.id); return Container( key: Key('$exam-exam'), margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), @@ -133,7 +143,16 @@ class ExamsPageViewState extends GeneralPageViewState { color: isHidden ? Theme.of(context).hintColor : Theme.of(context).scaffoldBackgroundColor, - child: ExamRow(exam: exam, teacher: '', mainPage: false), + child: ExamRow( + exam: exam, + teacher: '', + mainPage: false, + onChangeVisibility: () { + setState(() { + hiddenExams = PreferencesController.getHiddenExams(); + }); + }, + ), ), ); } @@ -143,4 +162,19 @@ class ExamsPageViewState extends GeneralPageViewState { return Provider.of(context, listen: false) .forceRefresh(context); } + + @override + String? getTitle() => S.of(context).nav_title(NavigationItem.navExams.route); + + @override + Widget? getTopRightButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: ExamFilterButton( + () => setState(() { + filteredExamTypes = PreferencesController.getFilteredExams(); + }), + ), + ); + } } diff --git a/uni/lib/view/exams/widgets/day_title.dart b/uni/lib/view/exams/widgets/day_title.dart deleted file mode 100644 index 56710245a..000000000 --- a/uni/lib/view/exams/widgets/day_title.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class DayTitle extends StatelessWidget { - const DayTitle({ - required this.day, - required this.weekDay, - required this.month, - super.key, - }); - final String day; - final String weekDay; - final String month; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.only(top: 15, bottom: 3), - alignment: Alignment.center, - child: Text( - '$weekDay, $day de $month', - style: Theme.of(context).textTheme.titleLarge, - ), - ); - } -} diff --git a/uni/lib/view/exams/widgets/exam_filter_button.dart b/uni/lib/view/exams/widgets/exam_filter_button.dart new file mode 100644 index 000000000..b05f0ce61 --- /dev/null +++ b/uni/lib/view/exams/widgets/exam_filter_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; +import 'package:uni/view/exams/widgets/exam_filter_form.dart'; + +class ExamFilterButton extends StatelessWidget { + const ExamFilterButton(this.onDismissFilterDialog, {super.key}); + + final void Function() onDismissFilterDialog; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + showDialog( + context: context, + builder: (_) { + final filteredExamsTypes = PreferencesController.getFilteredExams(); + return ExamFilterForm( + Map.from(filteredExamsTypes), + onDismissFilterDialog, + ); + }, + ); + }, + ); + } +} diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 7f8396e01..95d371ad1 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/lazy/exam_provider.dart'; class ExamFilterForm extends StatefulWidget { - const ExamFilterForm(this.filteredExamsTypes, {super.key}); + const ExamFilterForm(this.filteredExamsTypes, this.onDismiss, {super.key}); final Map filteredExamsTypes; + final void Function() onDismiss; @override ExamFilterFormState createState() => ExamFilterFormState(); @@ -32,8 +32,10 @@ class ExamFilterFormState extends State { ElevatedButton( child: Text(S.of(context).confirm), onPressed: () { - Provider.of(context, listen: false) - .setFilteredExams(widget.filteredExamsTypes); + PreferencesController.saveFilteredExams( + widget.filteredExamsTypes, + ); + widget.onDismiss(); 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 deleted file mode 100644 index 1ea516678..000000000 --- a/uni/lib/view/exams/widgets/exam_filter_menu.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/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 { - const ExamFilterMenu({super.key}); - - @override - ExamFilterMenuState createState() => ExamFilterMenuState(); -} - -class ExamFilterMenuState extends State { - @override - Widget build(BuildContext context) { - return IconButton( - icon: const Icon(Icons.filter_alt), - onPressed: () { - showDialog( - context: context, - builder: (_) { - final examProvider = - Provider.of(context, listen: false); - final filteredExamsTypes = examProvider.filteredExamsTypes; - return ChangeNotifierProvider.value( - value: examProvider, - child: ExamFilterForm( - Map.from(filteredExamsTypes), - ), - ); - }, - ); - }, - ); - } -} diff --git a/uni/lib/view/exams/widgets/exam_page_title.dart b/uni/lib/view/exams/widgets/exam_page_title.dart deleted file mode 100644 index ff3d46e64..000000000 --- a/uni/lib/view/exams/widgets/exam_page_title.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; -import 'package:uni/view/exams/widgets/exam_filter_menu.dart'; - -class ExamPageTitle extends StatelessWidget { - const ExamPageTitle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PageTitle( - name: S.of(context).nav_title(DrawerItem.navExams.title), - center: false, - pad: false, - ), - const Material(child: ExamFilterMenu()), - ], - ), - ); - } -} diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index 83457ad84..cfe2a8de7 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -1,9 +1,8 @@ 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/controller/local_storage/preferences_controller.dart'; import 'package:uni/model/entities/exam.dart'; -import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/view/exams/widgets/exam_time.dart'; import 'package:uni/view/exams/widgets/exam_title.dart'; @@ -12,12 +11,16 @@ class ExamRow extends StatefulWidget { required this.exam, required this.teacher, required this.mainPage, + required this.onChangeVisibility, super.key, }); + final Exam exam; final String teacher; final bool mainPage; + final void Function() onChangeVisibility; + @override State createState() { return _ExamRowState(); @@ -25,10 +28,16 @@ class ExamRow extends StatefulWidget { } class _ExamRowState extends State { + bool isHidden = false; + + @override + void initState() { + super.initState(); + isHidden = PreferencesController.getHiddenExams().contains(widget.exam.id); + } + @override Widget build(BuildContext context) { - final isHidden = - Provider.of(context).hiddenExams.contains(widget.exam.id); final roomsKey = '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.beginTime}-' '${widget.exam.endTime}'; @@ -45,11 +54,13 @@ class _ExamRowState extends State { children: [ Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - ExamTime( - begin: widget.exam.beginTime, - ), - ], + children: widget.exam.beginTime != '00:00' + ? [ + ExamTime( + begin: widget.exam.beginTime, + ), + ] + : [], ), ExamTitle( subject: widget.exam.subject, @@ -69,12 +80,26 @@ class _ExamRowState extends State { tooltip: isHidden ? 'Mostrar na Área Pessoal' : 'Ocultar da Área Pessoal', - onPressed: () => setState(() { - Provider.of( - context, - listen: false, - ).toggleHiddenExam(widget.exam.id); - }), + onPressed: () async { + final hiddenExams = + PreferencesController.getHiddenExams(); + + if (hiddenExams.contains(widget.exam.id)) { + hiddenExams.remove(widget.exam.id); + } else { + hiddenExams.add(widget.exam.id); + } + + setState(() { + PreferencesController.saveHiddenExams( + hiddenExams, + ); + setState(() { + isHidden = !isHidden; + }); + widget.onChangeVisibility(); + }); + }, ), IconButton( icon: Icon(MdiIcons.calendarPlus, size: 30), @@ -99,7 +124,7 @@ class _ExamRowState extends State { } Widget? getExamRooms(BuildContext context) { - if (widget.exam.rooms[0] == '') return null; + if (widget.exam.rooms.isEmpty || widget.exam.rooms[0] == '') return null; return Wrap( spacing: 13, children: roomsList(context, widget.exam.rooms), diff --git a/uni/lib/view/faculty/faculty.dart b/uni/lib/view/faculty/faculty.dart new file mode 100644 index 000000000..5db7bf9b0 --- /dev/null +++ b/uni/lib/view/faculty/faculty.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/calendar/widgets/calendar_card.dart'; +import 'package:uni/view/common_widgets/generic_expansion_card.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/faculty/widgets/academic_services_card.dart'; +import 'package:uni/view/faculty/widgets/copy_center_card.dart'; +import 'package:uni/view/faculty/widgets/dona_bia_card.dart'; +import 'package:uni/view/faculty/widgets/infodesk_card.dart'; +import 'package:uni/view/faculty/widgets/multimedia_center_card.dart'; +import 'package:uni/view/faculty/widgets/other_links_card.dart'; +import 'package:uni/view/faculty/widgets/sigarra_links_card.dart'; +import 'package:uni/view/library/widgets/library_occupation_card.dart'; + +class FacultyPageView extends StatefulWidget { + const FacultyPageView({super.key}); + + @override + State createState() => FacultyPageViewState(); +} + +class FacultyPageViewState extends GeneralPageViewState { + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navFaculty.route); + + @override + Widget getBody(BuildContext context) { + return ListView( + children: [ + LibraryOccupationCard(), + CalendarCard(), + ...getUtilsSection(), + ], + ); + } + + List getUtilsSection() { + return const [ + AcademicServicesCard(), + InfoDeskCard(), + DonaBiaCard(), + CopyCenterCard(), + MultimediaCenterCard(), + SigarraLinksCard(), + OtherLinksCard(), + ]; + } + + @override + Future onRefresh(BuildContext context) async { + return Provider.of(context, listen: false) + .forceRefresh(context); + } +} diff --git a/uni/lib/view/useful_info/widgets/academic_services_card.dart b/uni/lib/view/faculty/widgets/academic_services_card.dart similarity index 83% rename from uni/lib/view/useful_info/widgets/academic_services_card.dart rename to uni/lib/view/faculty/widgets/academic_services_card.dart index 212b670e3..535df4280 100644 --- a/uni/lib/view/useful_info/widgets/academic_services_card.dart +++ b/uni/lib/view/faculty/widgets/academic_services_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/text_components.dart'; +import 'package:uni/view/faculty/widgets/text_components.dart'; class AcademicServicesCard extends GenericExpansionCard { const AcademicServicesCard({super.key}); @@ -12,7 +12,7 @@ class AcademicServicesCard extends GenericExpansionCard { return Column( children: [ h1( - S.of(context).nav_title(DrawerItem.navSchedule.title), + S.of(context).nav_title(NavigationItem.navSchedule.route), context, initial: true, ), diff --git a/uni/lib/view/useful_info/widgets/copy_center_card.dart b/uni/lib/view/faculty/widgets/copy_center_card.dart similarity index 85% rename from uni/lib/view/useful_info/widgets/copy_center_card.dart rename to uni/lib/view/faculty/widgets/copy_center_card.dart index 055610a55..b555df81a 100644 --- a/uni/lib/view/useful_info/widgets/copy_center_card.dart +++ b/uni/lib/view/faculty/widgets/copy_center_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/text_components.dart'; +import 'package:uni/view/faculty/widgets/text_components.dart'; class CopyCenterCard extends GenericExpansionCard { const CopyCenterCard({super.key}); @@ -12,7 +12,7 @@ class CopyCenterCard extends GenericExpansionCard { return Column( children: [ h1( - S.of(context).nav_title(DrawerItem.navSchedule.title), + S.of(context).nav_title(NavigationItem.navSchedule.route), context, initial: true, ), diff --git a/uni/lib/view/useful_info/widgets/dona_bia_card.dart b/uni/lib/view/faculty/widgets/dona_bia_card.dart similarity index 83% rename from uni/lib/view/useful_info/widgets/dona_bia_card.dart rename to uni/lib/view/faculty/widgets/dona_bia_card.dart index e08b30307..fdabeeb2f 100644 --- a/uni/lib/view/useful_info/widgets/dona_bia_card.dart +++ b/uni/lib/view/faculty/widgets/dona_bia_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/text_components.dart'; +import 'package:uni/view/faculty/widgets/text_components.dart'; class DonaBiaCard extends GenericExpansionCard { const DonaBiaCard({super.key}); @@ -12,7 +12,7 @@ class DonaBiaCard extends GenericExpansionCard { return Column( children: [ h1( - S.of(context).nav_title(DrawerItem.navSchedule.title), + S.of(context).nav_title(NavigationItem.navSchedule.route), context, initial: true, ), diff --git a/uni/lib/view/useful_info/widgets/infodesk_card.dart b/uni/lib/view/faculty/widgets/infodesk_card.dart similarity index 83% rename from uni/lib/view/useful_info/widgets/infodesk_card.dart rename to uni/lib/view/faculty/widgets/infodesk_card.dart index ddbed1d13..a256ff999 100644 --- a/uni/lib/view/useful_info/widgets/infodesk_card.dart +++ b/uni/lib/view/faculty/widgets/infodesk_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/text_components.dart'; +import 'package:uni/view/faculty/widgets/text_components.dart'; class InfoDeskCard extends GenericExpansionCard { const InfoDeskCard({super.key}); @@ -12,7 +12,7 @@ class InfoDeskCard extends GenericExpansionCard { return Column( children: [ h1( - S.of(context).nav_title(DrawerItem.navSchedule.title), + S.of(context).nav_title(NavigationItem.navSchedule.route), context, initial: true, ), diff --git a/uni/lib/view/useful_info/widgets/link_button.dart b/uni/lib/view/faculty/widgets/link_button.dart similarity index 87% rename from uni/lib/view/useful_info/widgets/link_button.dart rename to uni/lib/view/faculty/widgets/link_button.dart index 4fc901bfb..f146bb074 100644 --- a/uni/lib/view/useful_info/widgets/link_button.dart +++ b/uni/lib/view/faculty/widgets/link_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/controller/networking/url_launcher.dart'; class LinkButton extends StatelessWidget { const LinkButton({ @@ -27,7 +27,7 @@ class LinkButton extends StatelessWidget { .headlineSmall! .copyWith(decoration: TextDecoration.underline), ), - onTap: () => launchUrl(Uri.parse(link)), + onTap: () => launchUrlWithToast(context, link), ), ), ], diff --git a/uni/lib/view/useful_info/widgets/multimedia_center_card.dart b/uni/lib/view/faculty/widgets/multimedia_center_card.dart similarity index 83% rename from uni/lib/view/useful_info/widgets/multimedia_center_card.dart rename to uni/lib/view/faculty/widgets/multimedia_center_card.dart index 46bda1568..66c2f7406 100644 --- a/uni/lib/view/useful_info/widgets/multimedia_center_card.dart +++ b/uni/lib/view/faculty/widgets/multimedia_center_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/text_components.dart'; +import 'package:uni/view/faculty/widgets/text_components.dart'; class MultimediaCenterCard extends GenericExpansionCard { const MultimediaCenterCard({super.key}); @@ -12,7 +12,7 @@ class MultimediaCenterCard extends GenericExpansionCard { return Column( children: [ h1( - S.of(context).nav_title(DrawerItem.navSchedule.title), + S.of(context).nav_title(NavigationItem.navSchedule.route), context, initial: true, ), diff --git a/uni/lib/view/useful_info/widgets/other_links_card.dart b/uni/lib/view/faculty/widgets/other_links_card.dart similarity index 93% rename from uni/lib/view/useful_info/widgets/other_links_card.dart rename to uni/lib/view/faculty/widgets/other_links_card.dart index 47ae82046..d4960e737 100644 --- a/uni/lib/view/useful_info/widgets/other_links_card.dart +++ b/uni/lib/view/faculty/widgets/other_links_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/link_button.dart'; +import 'package:uni/view/faculty/widgets/link_button.dart'; /// Manages the 'Current account' section inside the user's page (accessible /// through the top-right widget with the user picture) diff --git a/uni/lib/view/useful_info/widgets/sigarra_links_card.dart b/uni/lib/view/faculty/widgets/sigarra_links_card.dart similarity index 82% rename from uni/lib/view/useful_info/widgets/sigarra_links_card.dart rename to uni/lib/view/faculty/widgets/sigarra_links_card.dart index bd1ec3813..32c4fd76b 100644 --- a/uni/lib/view/useful_info/widgets/sigarra_links_card.dart +++ b/uni/lib/view/faculty/widgets/sigarra_links_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; -import 'package:uni/view/useful_info/widgets/link_button.dart'; +import 'package:uni/view/faculty/widgets/link_button.dart'; /// Manages the 'Current account' section inside the user's page (accessible /// through the top-right widget with the user picture) @@ -34,11 +34,6 @@ class SigarraLinksCard extends GenericExpansionCard { link: 'https://sigarra.up.pt/feup/pt/inqueritos_geral.inqueritos_list', ), - LinkButton( - title: S.of(context).school_calendar, - link: - 'https://sigarra.up.pt/feup/pt/web_base.gera_pagina?p_pagina=p%c3%a1gina%20est%c3%a1tica%20gen%c3%a9rica%20106', - ), ], ); } diff --git a/uni/lib/view/useful_info/widgets/text_components.dart b/uni/lib/view/faculty/widgets/text_components.dart similarity index 91% rename from uni/lib/view/useful_info/widgets/text_components.dart rename to uni/lib/view/faculty/widgets/text_components.dart index 345685277..7ef70043e 100644 --- a/uni/lib/view/useful_info/widgets/text_components.dart +++ b/uni/lib/view/faculty/widgets/text_components.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/controller/networking/url_launcher.dart'; Container h1(String text, BuildContext context, {bool initial = false}) { final marginTop = initial ? 15.0 : 30.0; @@ -44,7 +44,7 @@ Container infoText( .bodyLarge! .apply(color: Theme.of(context).colorScheme.tertiary), ), - onTap: () => link != '' ? launchUrl(Uri.parse(link)) : null, + onTap: () => launchUrlWithToast(context, link), ), ), ); diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index b5c5d9d99..15b3298ae 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uni/model/providers/lazy/home_page_provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; +import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/profile_button.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/widgets/top_navigation_bar.dart'; import 'package:uni/view/home/widgets/main_cards_list.dart'; +import 'package:uni/view/home/widgets/tracking_banner.dart'; +import 'package:uni/view/home/widgets/uni_icon.dart'; class HomePageView extends StatefulWidget { const HomePageView({super.key}); @@ -11,16 +15,52 @@ class HomePageView extends StatefulWidget { State createState() => HomePageViewState(); } -/// Tracks the state of Home page. class HomePageViewState extends GeneralPageViewState { + bool isBannerViewed = true; + List favoriteCardTypes = + PreferencesController.getFavoriteCards(); + + @override + void initState() { + super.initState(); + checkBannerViewed(); + } + + Future checkBannerViewed() async { + setState(() { + isBannerViewed = PreferencesController.isDataCollectionBannerViewed(); + }); + } + + Future setBannerViewed() async { + await PreferencesController.setDataCollectionBannerViewed(isViewed: true); + await checkBannerViewed(); + } + + void setFavoriteCards(List favorites) { + setState(() { + favoriteCardTypes = favorites; + }); + PreferencesController.saveFavoriteCards(favorites); + } + @override Widget getBody(BuildContext context) { - return const MainCardsList(); + return Column( + children: [ + Visibility( + visible: !isBannerViewed, + child: TrackingBanner(setBannerViewed), + ), + Expanded( + child: MainCardsList(favoriteCardTypes, setFavoriteCards), + ), + ], + ); } @override Future onRefresh(BuildContext context) async { - final favoriteCardTypes = context.read().favoriteCards; final cards = favoriteCardTypes .map( (e) => @@ -32,4 +72,18 @@ class HomePageViewState extends GeneralPageViewState { card.onRefresh(context); } } + + @override + String? getTitle() => null; + + @override + AppTopNavbar? getTopNavbar(BuildContext context) { + return const AppTopNavbar( + leftButton: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: UniIcon(), + ), + rightButton: ProfileButton(), + ); + } } diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 2345458cc..70a8e9218 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -3,8 +3,7 @@ import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; -import 'package:uni/model/request_status.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_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'; @@ -13,6 +12,8 @@ import 'package:uni/view/lazy_consumer.dart'; /// Manages the bus stops card displayed on the user's personal area class BusStopCard extends GenericCard { + BusStopCard({super.key}); + const BusStopCard.fromEditingInformation( super.key, { required super.editingMode, @@ -21,73 +22,16 @@ class BusStopCard extends GenericCard { @override String getTitle(BuildContext context) => - S.of(context).nav_title(DrawerItem.navStops.title); + S.of(context).nav_title(NavigationItem.navStops.route); @override Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${DrawerItem.navStops.title}'); + Navigator.pushNamed(context, '/${NavigationItem.navStops.route}'); @override Widget buildCardContent(BuildContext context) { - 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, - RequestStatus busStopStatus, -) { - switch (busStopStatus) { - case RequestStatus.successful: - if (stopData.isNotEmpty) { - return Column( - children: [ - getCardTitle(context), - getBusStopsInfo(context, stopData), - ], - ); - } else { - return Container( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).buses_personalize, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall!.apply(), - ), - IconButton( - icon: const Icon(Icons.settings), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage(), - ), - ), - ), - ], - ), - ); - } - case RequestStatus.busy: - return Column( + return LazyConsumer>( + contentLoadingWidget: Column( children: [ getCardTitle(context), Container( @@ -95,21 +39,43 @@ Widget getCardContent( child: const Center(child: CircularProgressIndicator()), ), ], - ); - case RequestStatus.failed: - case RequestStatus.none: - return Column( + ), + builder: (context, stopData) => Column( children: [ getCardTitle(context), - Container( - padding: const EdgeInsets.all(8), - child: Text( - S.of(context).bus_error, - style: Theme.of(context).textTheme.titleMedium, - ), - ), + getBusStopsInfo(context, stopData), ], - ); + ), + hasContent: (Map busStops) => busStops.isNotEmpty, + onNullContent: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).buses_personalize, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall!.apply(), + ), + IconButton( + icon: const Icon(Icons.settings), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage(), + ), + ), + ), + ], + ), + ), + ); + } + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false).forceRefresh(context); } } @@ -154,7 +120,9 @@ List getEachBusStopInfo( BuildContext context, Map stopData, ) { - final rows = [const LastUpdateTimeStamp()]; + final rows = [ + 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 0b3199cc4..e41da412f 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -1,21 +1,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/model/entities/app_locale.dart'; import 'package:uni/model/entities/exam.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/utils/navigation_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/common_widgets/row_container.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/home/widgets/next_exams_card.dart'; +import 'package:uni/view/home/widgets/remaining_exams_card.dart'; import 'package:uni/view/lazy_consumer.dart'; -import 'package:uni/view/locale_notifier.dart'; -/// Manages the exam card section inside the personal area. class ExamCard extends GenericCard { ExamCard({super.key}); @@ -25,139 +20,103 @@ class ExamCard extends GenericCard { super.onDelete, }) : super.fromEditingInformation(); + static const int maxExamsToDisplay = 4; + @override String getTitle(BuildContext context) => - S.of(context).nav_title(DrawerItem.navExams.title); + S.of(context).nav_title(NavigationItem.navExams.route); @override Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${DrawerItem.navExams.title}'); + Navigator.pushNamed(context, '/${NavigationItem.navExams.route}'); @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 LazyConsumer( - builder: (context, examProvider) { - final filteredExams = examProvider.getFilteredExams(); - final hiddenExams = examProvider.hiddenExams; - final exams = filteredExams - .where((exam) => !hiddenExams.contains(exam.id)) - .toList(); - return RequestDependentWidgetBuilder( - status: examProvider.status, - builder: () => generateExams(exams, context), - hasContentPredicate: exams.isNotEmpty, + return StreamBuilder( + stream: PreferencesController.onHiddenExamsChange, + initialData: PreferencesController.getHiddenExams(), + builder: (context, snapshot) { + final hiddenExams = snapshot.data ?? []; + + return LazyConsumer>( + builder: (context, allExams) { + final visibleExams = + getVisibleExams(allExams, hiddenExams).toList(); + + final nextExams = getPrimaryExams( + visibleExams, + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + NextExamsWidget(exams: nextExams), + if (nextExams.length < maxExamsToDisplay && + visibleExams.length > nextExams.length) + Column( + children: [ + Container( + margin: const EdgeInsets.only( + right: 80, + left: 80, + top: 7, + bottom: 7, + ), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + ), + RemainingExamsWidget( + exams: visibleExams + .where((exam) => !nextExams.contains(exam)) + .take(maxExamsToDisplay - nextExams.length) + .toList(), + ), + ], + ), + ], + ); + }, + hasContent: (allExams) => + getVisibleExams(allExams, hiddenExams).isNotEmpty, onNullContent: Center( child: Text( S.of(context).no_selected_exams, style: Theme.of(context).textTheme.titleLarge, ), ), - contentLoadingWidget: const ExamCardShimmer().build(context), + contentLoadingWidget: const ExamCardShimmer(), ); }, ); } - /// Returns a widget with all the exams. - Widget generateExams(List exams, BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: getExamRows(context, exams), - ); + Iterable getVisibleExams( + List allExams, + List hiddenExams, + ) { + final hiddenExamsSet = Set.from(hiddenExams); + return allExams.where((exam) => !hiddenExamsSet.contains(exam.id)); } - /// Returns a list of widgets with the primary and secondary exams to - /// be displayed in the exam card. - List getExamRows(BuildContext context, List exams) { - final rows = []; - for (var i = 0; i < 1 && i < exams.length; i++) { - rows.add(createRowFromExam(context, exams[i])); - } - if (exams.length > 1) { - rows.add( - Container( - margin: - const EdgeInsets.only(right: 80, left: 80, top: 15, bottom: 7), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.5, - color: Theme.of(context).dividerColor, - ), - ), - ), - ), - ); - } - for (var i = 1; i < 4 && i < exams.length; i++) { - rows.add(createSecondaryRowFromExam(context, exams[i])); - } - return rows; + List getPrimaryExams(List visibleExams) { + return visibleExams + .where((exam) => isSameDay(visibleExams[0].begin, exam.begin)) + .toList(); } - /// Creates a row with the closest exam (which appears separated from the - /// others in the card). - Widget createRowFromExam(BuildContext context, Exam exam) { - final locale = Provider.of(context).getLocale(); - return Column( - children: [ - if (locale == AppLocale.pt) ...[ - DateRectangle( - date: '''${exam.weekDay(locale)}, ''' - '''${exam.begin.day} de ${exam.month(locale)}''', - ), - ] else ...[ - DateRectangle( - date: '''${exam.weekDay(locale)}, ''' - '''${exam.begin.day} ${exam.month(locale)}''', - ), - ], - RowContainer( - child: ExamRow( - exam: exam, - teacher: '', - mainPage: true, - ), - ), - ], - ); - } - - /// Creates a row for the exams which will be displayed under the closest - /// date exam with a separator between them. - Widget createSecondaryRowFromExam(BuildContext context, Exam exam) { - final locale = Provider.of(context).getLocale(); - return Container( - margin: const EdgeInsets.only(top: 8), - child: RowContainer( - color: Theme.of(context).colorScheme.background, - child: Container( - padding: const EdgeInsets.all(11), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${exam.begin.day} de ${exam.month(locale)}', - style: Theme.of(context).textTheme.bodyLarge, - ), - ExamTitle( - subject: exam.subject, - type: exam.type, - reverseOrder: true, - ), - ], - ), - ), - ), - ); + bool isSameDay(DateTime? dateA, DateTime? dateB) { + return dateA?.year == dateB?.year && + dateA?.month == dateB?.month && + dateA?.day == dateB?.day; } } diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 241f6a2f1..ad9167dbc 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/model/providers/lazy/home_page_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; @@ -12,9 +10,9 @@ import 'package:uni/view/home/widgets/exam_card.dart'; import 'package:uni/view/home/widgets/exit_app_dialog.dart'; import 'package:uni/view/home/widgets/restaurant_card.dart'; import 'package:uni/view/home/widgets/schedule_card.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'; +import 'package:uni/view/profile/widgets/print_info_card.dart'; typedef CardCreator = GenericCard Function( Key key, { @@ -22,66 +20,70 @@ typedef CardCreator = GenericCard Function( void Function()? onDelete, }); -class MainCardsList extends StatelessWidget { - const MainCardsList({super.key}); +class MainCardsList extends StatefulWidget { + const MainCardsList( + this.favoriteCardTypes, + this.saveFavoriteCards, { + super.key, + }); + + final List favoriteCardTypes; + final void Function(List) saveFavoriteCards; static Map cardCreators = { FavoriteWidgetType.schedule: ScheduleCard.fromEditingInformation, FavoriteWidgetType.exams: ExamCard.fromEditingInformation, FavoriteWidgetType.account: AccountInfoCard.fromEditingInformation, - - // TODO(bdmendes): Bring print card back when it is ready - /*FavoriteWidgetType.printBalance: (k, em, od) => - PrintInfoCard.fromEditingInformation(k, em, od),*/ - + FavoriteWidgetType.printBalance: PrintInfoCard.fromEditingInformation, FavoriteWidgetType.busStops: BusStopCard.fromEditingInformation, FavoriteWidgetType.restaurant: RestaurantCard.fromEditingInformation, FavoriteWidgetType.libraryOccupation: LibraryOccupationCard.fromEditingInformation, }; + @override + State createState() { + return MainCardsListState(); + } +} + +class MainCardsListState extends State { + bool isEditing = false; + @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, homePageProvider) => Scaffold( - body: BackButtonExitWrapper( - child: SizedBox( - height: MediaQuery.of(context).size.height, - child: homePageProvider.isEditing - ? ReorderableListView( - onReorder: (oldIndex, newIndex) => reorderCard( - oldIndex, - newIndex, - homePageProvider.favoriteCards, - context, - ), - header: createTopBar(context, homePageProvider), - children: favoriteCardsFromTypes( - homePageProvider.favoriteCards, + return Scaffold( + body: BackButtonExitWrapper( + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: isEditing + ? ReorderableListView( + onReorder: reorderCard, + header: createTopBar(context), + children: favoriteCardsFromTypes( + widget.favoriteCardTypes, + context, + ), + ) + : ListView( + padding: EdgeInsets.zero, + children: [ + createTopBar(context), + ...favoriteCardsFromTypes( + widget.favoriteCardTypes, context, - homePageProvider, ), - ) - : ListView( - children: [ - createTopBar(context, homePageProvider), - ...favoriteCardsFromTypes( - homePageProvider.favoriteCards, - context, - homePageProvider, - ), - ], - ), - ), + ], + ), ), - floatingActionButton: - homePageProvider.isEditing ? createActionButton(context) : null, ), + floatingActionButton: isEditing ? createActionButton(context) : null, ); } Widget createActionButton(BuildContext context) { return FloatingActionButton( + backgroundColor: Theme.of(context).colorScheme.secondary, onPressed: () => showDialog( context: context, builder: (BuildContext context) { @@ -113,14 +115,11 @@ class MainCardsList extends StatelessWidget { } List getCardAdders(BuildContext context) { - final session = - Provider.of(context, listen: false).session; - final favorites = - Provider.of(context, listen: false).favoriteCards; + final session = Provider.of(context, listen: false).state!; - final possibleCardAdditions = cardCreators.entries + final possibleCardAdditions = MainCardsList.cardCreators.entries .where((e) => e.key.isVisible(session.faculties)) - .where((e) => !favorites.contains(e.key)) + .where((e) => !widget.favoriteCardTypes.contains(e.key)) .map( (e) => DecoratedBox( decoration: const BoxDecoration(), @@ -147,10 +146,9 @@ class MainCardsList extends StatelessWidget { Widget createTopBar( BuildContext context, - HomePageProvider editingModeProvider, ) { return Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 5), + padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -159,18 +157,24 @@ class MainCardsList extends StatelessWidget { center: false, pad: false, ), - GestureDetector( - onTap: () => Provider.of(context, listen: false) - .setHomePageEditingMode( - editingMode: !editingModeProvider.isEditing, - ), - child: Text( - editingModeProvider.isEditing - ? S.of(context).edit_on - : S.of(context).edit_off, - style: Theme.of(context).textTheme.bodySmall, + if (isEditing) + ElevatedButton( + onPressed: () => setState(() { + isEditing = false; + }), + child: Text( + S.of(context).edit_on, + ), + ) + else + OutlinedButton( + onPressed: () => setState(() { + isEditing = true; + }), + child: Text( + S.of(context).edit_off, + ), ), - ), ], ), ); @@ -179,18 +183,17 @@ class MainCardsList extends StatelessWidget { List favoriteCardsFromTypes( List cardTypes, BuildContext context, - HomePageProvider homePageProvider, ) { final userSession = - Provider.of(context, listen: false).session; + Provider.of(context, listen: false).state; return cardTypes - .where((type) => type.isVisible(userSession.faculties)) - .where((type) => cardCreators.containsKey(type)) + .where((type) => type.isVisible(userSession?.faculties ?? [])) + .where((type) => MainCardsList.cardCreators.containsKey(type)) .map((type) { final i = cardTypes.indexOf(type); - return cardCreators[type]!( + return MainCardsList.cardCreators[type]!( Key(i.toString()), - editingMode: homePageProvider.isEditing, + editingMode: isEditing, onDelete: () => removeCardIndexFromFavorites(i, context), ); }).toList(); @@ -199,38 +202,27 @@ class MainCardsList extends StatelessWidget { void reorderCard( int oldIndex, int newIndex, - List favorites, - BuildContext context, ) { - final tmp = favorites[oldIndex]; - favorites + final newFavorites = + List.from(widget.favoriteCardTypes); + final tmp = newFavorites[oldIndex]; + newFavorites ..removeAt(oldIndex) ..insert(oldIndex < newIndex ? newIndex - 1 : newIndex, tmp); - saveFavoriteCards(context, favorites); + widget.saveFavoriteCards(newFavorites); } void removeCardIndexFromFavorites(int i, BuildContext context) { - final favorites = Provider.of(context, listen: false) - .favoriteCards + final favorites = List.from(widget.favoriteCardTypes) ..removeAt(i); - saveFavoriteCards(context, favorites); + widget.saveFavoriteCards(favorites); } void addCardToFavorites(FavoriteWidgetType type, BuildContext context) { - final favorites = - Provider.of(context, listen: false).favoriteCards; + final favorites = List.from(widget.favoriteCardTypes); if (!favorites.contains(type)) { favorites.add(type); } - saveFavoriteCards(context, favorites); - } - - void saveFavoriteCards( - BuildContext context, - List favorites, - ) { - Provider.of(context, listen: false) - .setFavoriteCards(favorites); - AppSharedPreferences.saveFavoriteCards(favorites); + widget.saveFavoriteCards(favorites); } } diff --git a/uni/lib/view/home/widgets/next_exams_card.dart b/uni/lib/view/home/widgets/next_exams_card.dart new file mode 100644 index 000000000..88d99a672 --- /dev/null +++ b/uni/lib/view/home/widgets/next_exams_card.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/entities/exam.dart'; +import 'package:uni/utils/date_time_formatter.dart'; +import 'package:uni/view/common_widgets/date_rectangle.dart'; +import 'package:uni/view/common_widgets/row_container.dart'; +import 'package:uni/view/exams/widgets/exam_row.dart'; +import 'package:uni/view/locale_notifier.dart'; + +class NextExamsWidget extends StatelessWidget { + const NextExamsWidget({required this.exams, super.key}); + + final List exams; + + @override + Widget build(BuildContext context) { + final locale = Provider.of(context).getLocale(); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DateRectangle( + date: exams.isNotEmpty ? exams.first.begin.formattedDate(locale) : '', + ), + Column( + children: exams.map((exam) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: RowContainer( + child: ExamRow( + exam: exam, + teacher: '', + mainPage: true, + onChangeVisibility: () {}, + ), + ), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/uni/lib/view/home/widgets/remaining_exams_card.dart b/uni/lib/view/home/widgets/remaining_exams_card.dart new file mode 100644 index 000000000..f6fdb1eb0 --- /dev/null +++ b/uni/lib/view/home/widgets/remaining_exams_card.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/entities/exam.dart'; +import 'package:uni/utils/date_time_formatter.dart'; +import 'package:uni/view/common_widgets/row_container.dart'; +import 'package:uni/view/exams/widgets/exam_title.dart'; +import 'package:uni/view/locale_notifier.dart'; + +class RemainingExamsWidget extends StatelessWidget { + const RemainingExamsWidget({required this.exams, super.key}); + + final List exams; + + @override + Widget build(BuildContext context) { + return Column( + children: exams.map((exam) { + final locale = Provider.of(context).getLocale(); + return Container( + margin: const EdgeInsets.only(top: 8), + child: RowContainer( + color: Theme.of(context).colorScheme.background, + child: Container( + padding: const EdgeInsets.all(11), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + exam.begin.formattedDate(locale), + style: Theme.of(context).textTheme.bodyLarge, + ), + ExamTitle( + subject: exam.subject, + type: exam.type, + reverseOrder: true, + ), + ], + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 010264d9b..ac16adc59 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/meal.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/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_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/common_widgets/row_container.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/restaurant/widgets/restaurant_slot.dart'; @@ -23,11 +22,11 @@ class RestaurantCard extends GenericCard { @override String getTitle(BuildContext context) => - S.of(context).nav_title(DrawerItem.navRestaurants.title); + S.of(context).nav_title(NavigationItem.navRestaurants.route); @override Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${DrawerItem.navRestaurants.title}'); + Navigator.pushNamed(context, '/${NavigationItem.navRestaurants.route}'); @override void onRefresh(BuildContext context) { @@ -37,47 +36,67 @@ class RestaurantCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( - builder: (context, restaurantProvider) { - final favoriteRestaurants = restaurantProvider.restaurants + return LazyConsumer>( + builder: (context, restaurants) { + final favoriteRestaurants = restaurants .where( - (restaurant) => restaurantProvider.favoriteRestaurants + (restaurant) => PreferencesController.getFavoriteRestaurants() .contains(restaurant.name), ) .toList(); - return RequestDependentWidgetBuilder( - status: restaurantProvider.status, - builder: () => generateRestaurants(favoriteRestaurants, context), - hasContentPredicate: favoriteRestaurants.isNotEmpty, - onNullContent: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 15, bottom: 10), - child: Center( - child: Text( - S.of(context).no_favorite_restaurants, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - OutlinedButton( - onPressed: () => Navigator.pushNamed( - context, - '/${DrawerItem.navRestaurants.title}', - ), - child: Text(S.of(context).add), + return generateRestaurants(favoriteRestaurants, context); + }, + hasContent: (restaurants) => + PreferencesController.getFavoriteRestaurants().isNotEmpty, + onNullContent: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: Center( + child: Text( + S.of(context).no_favorite_restaurants, + style: Theme.of(context).textTheme.titleMedium, ), - ], + ), ), - ); - }, + OutlinedButton( + onPressed: () => Navigator.pushNamed( + context, + '/${NavigationItem.navRestaurants.route}', + ), + child: Text(S.of(context).add), + ), + ], + ), ); } - Widget generateRestaurants(List data, BuildContext context) { + Widget generateRestaurants( + List restaurants, + BuildContext context, + ) { final weekDay = DateTime.now().weekday; final offset = (weekDay - 1) % 7; - final restaurants = data; + + if (restaurants + .map((e) => e.meals[DayOfWeek.values[offset]]) + .every((element) => element?.isEmpty ?? true)) { + return Column( + children: [ + const SizedBox( + height: 15, + ), + Text( + S.of(context).no_menus, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox( + height: 15, + ), + ], + ); + } + return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -108,41 +127,29 @@ class RestaurantCard extends GenericCard { Center( child: Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.fromLTRB(12, 20, 12, 5), + padding: const EdgeInsets.fromLTRB(10, 15, 5, 10), child: Text( restaurant.name, style: TextStyle( + fontSize: 16, color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w400, ), ), ), ), if (meals.isNotEmpty) - Card( - elevation: 0, - child: RowContainer( - borderColor: Colors.transparent, - color: const Color.fromARGB(0, 0, 0, 0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: createRestaurantRows(meals, context), - ), - ), + Column( + mainAxisSize: MainAxisSize.min, + children: createRestaurantRows(meals, context), ) else - Card( - elevation: 0, - child: RowContainer( - borderColor: Colors.transparent, - color: const Color.fromARGB(0, 0, 0, 0), - child: Container( - padding: const EdgeInsets.fromLTRB(9, 0, 0, 0), - width: 400, - child: Text(S.of(context).no_menu_info), - ), - ), + Container( + padding: const EdgeInsets.fromLTRB(9, 0, 0, 0), + width: 400, + child: Text(S.of(context).no_menu_info), ), + const SizedBox(height: 10), ], ); } diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index f70d40b61..411051471 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -6,10 +6,10 @@ import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/model/utils/time/week.dart'; +import 'package:uni/utils/navigation_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/home/widgets/schedule_card_shimmer.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locale_notifier.dart'; @@ -35,40 +35,38 @@ class ScheduleCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( - builder: (context, lectureProvider) => RequestDependentWidgetBuilder( - status: lectureProvider.status, - builder: () => Column( - mainAxisSize: MainAxisSize.min, - children: getScheduleRows(context, lectureProvider.lectures), - ), - hasContentPredicate: lectureProvider.lectures.isNotEmpty, - onNullContent: Center( - child: Text( - S.of(context).no_classes, - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), + return LazyConsumer>( + builder: (context, lectures) => Column( + mainAxisSize: MainAxisSize.min, + children: getScheduleRows(context, lectures), + ), + hasContent: (lectures) => lectures.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_classes, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, ), - contentLoadingWidget: const ScheduleCardShimmer().build(context), ), + contentLoadingWidget: const ScheduleCardShimmer().build(context), ); } List getScheduleRows(BuildContext context, List lectures) { + final now = DateTime.now(); + final week = Week(start: now); + final rows = []; final lecturesByDay = lectures + .where((lecture) => week.contains(lecture.startTime)) .groupListsBy( (lecture) => lecture.startTime.weekday, ) .entries .toList() - .sortedBy((element) { - // Sort by day of the week, but next days come first - final dayDiff = element.key - DateTime.now().weekday; - return dayDiff >= 0 ? dayDiff - 7 : dayDiff; - }).toList(); + .sortedBy((element) => week.getWeekday(element.key)) + .toList(); for (final dayLectures in lecturesByDay.sublist(0, min(2, lecturesByDay.length))) { @@ -123,9 +121,9 @@ class ScheduleCard extends GenericCard { @override String getTitle(BuildContext context) => - S.of(context).nav_title(DrawerItem.navSchedule.title); + S.of(context).nav_title(NavigationItem.navSchedule.route); @override Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${DrawerItem.navSchedule.title}'); + Navigator.pushNamed(context, '/${NavigationItem.navSchedule.route}'); } diff --git a/uni/lib/view/home/widgets/tracking_banner.dart b/uni/lib/view/home/widgets/tracking_banner.dart new file mode 100644 index 000000000..a589bbb03 --- /dev/null +++ b/uni/lib/view/home/widgets/tracking_banner.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; + +class TrackingBanner extends StatelessWidget { + const TrackingBanner(this.onDismiss, {super.key}); + + final void Function() onDismiss; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).cardColor, + ), + margin: const EdgeInsets.all(10), + child: MaterialBanner( + padding: const EdgeInsets.all(15), + content: Text( + S.of(context).banner_info, + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.transparent, + actions: [ + TextButton( + onPressed: onDismiss, + child: const Text('OK', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + } +} diff --git a/uni/lib/view/home/widgets/uni_icon.dart b/uni/lib/view/home/widgets/uni_icon.dart new file mode 100644 index 000000000..ec62efebc --- /dev/null +++ b/uni/lib/view/home/widgets/uni_icon.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class UniIcon extends StatelessWidget { + const UniIcon({super.key}); + + @override + Widget build(BuildContext context) { + return ButtonTheme( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: const RoundedRectangleBorder(), + child: SvgPicture.asset( + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, + ), + 'assets/images/logo_dark.svg', + height: 35, + ), + ); + } +} diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index ccb2b0305..1fabbb6de 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -1,36 +1,50 @@ import 'dart:async'; import 'dart:io'; -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:sentry_flutter/sentry_flutter.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:uni/generated/l10n.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'; +import 'package:uni/model/request_status.dart'; +import 'package:uni/view/bug_report/bug_report.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 { +/// This widget also falls back to loading or error widgets if +/// the provider data is not ready or has thrown an error, respectively. +class LazyConsumer, T2> + extends StatelessWidget { const LazyConsumer({ required this.builder, + required this.hasContent, + required this.onNullContent, + this.contentLoadingWidget, super.key, }); - final Widget Function(BuildContext, T) builder; + final Widget Function(BuildContext, T2) builder; + final bool Function(T2) hasContent; + final Widget onNullContent; + final Widget? contentLoadingWidget; @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) async { - StateProviderNotifier? provider; + StateProviderNotifier? provider; try { - provider = Provider.of(context, listen: false); + provider = Provider.of(context, listen: false); } catch (e) { // The provider was not found. This should only happen in tests. - Logger().e('LazyConsumer: ${T.runtimeType} not found'); + Logger().e('LazyConsumer: ${T1.runtimeType} not found'); return; } @@ -57,27 +71,104 @@ class LazyConsumer extends StatelessWidget { } } - // Load data stored in the database immediately - try { - await provider.ensureInitializedFromStorage(); - } catch (exception, stackTrace) { - Logger().e( - 'Failed to initialize ${T.runtimeType} from storage: $exception', - ); - await Sentry.captureException(exception, stackTrace: stackTrace); - } - - // Finally, complete provider initialization if (context.mounted) { - await sessionFuture?.then((_) async { - await provider!.ensureInitializedFromRemote(context); - }); + await sessionFuture; + if (context.mounted) { + await provider.ensureInitialized(context); + } } }); - return Consumer( + return Consumer( builder: (context, provider, _) { - return builder(context, provider); + return requestDependantWidget(context, provider); + }, + ); + } + + Widget requestDependantWidget(BuildContext context, T1 provider) { + final showContent = + provider.state != null && hasContent(provider.state as T2); + + if (provider.requestStatus == RequestStatus.busy && !showContent) { + return loadingWidget(context); + } else if (provider.requestStatus == RequestStatus.failed) { + return requestFailedMessage(); + } + + return showContent + ? builder(context, provider.state as T2) + : Center( + child: 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) { + return const Center( + heightFactor: 3, + child: CircularProgressIndicator(), + ); + } + + if (connectivitySnapshot.data == ConnectivityResult.none) { + return Center( + heightFactor: 3, + child: Text( + S.of(context).check_internet, + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 10), + child: Center( + child: Text( + S.of(context).load_error, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + OutlinedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BugReportPageView(), + ), + ), + child: Text(S.of(context).report_error), + ), + ], + ); }, ); } diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index c5e48db7e..a86bc19c0 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -4,52 +4,43 @@ import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/library_occupation.dart'; import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_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/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; -class LibraryPageView extends StatefulWidget { - const LibraryPageView({super.key}); +class LibraryPage extends StatefulWidget { + const LibraryPage({super.key}); @override - State createState() => LibraryPageViewState(); + State createState() => LibraryPageState(); } -class LibraryPageViewState extends GeneralPageViewState { +class LibraryPageState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, libraryOccupationProvider) => - LibraryPage(libraryOccupationProvider.occupation), - ); - } - - @override - Future onRefresh(BuildContext context) { - return Provider.of(context, listen: false) - .forceRefresh(context); - } -} - -class LibraryPage extends StatelessWidget { - const LibraryPage(this.occupation, {super.key}); - final LibraryOccupation? occupation; - - @override - Widget build(BuildContext context) { return ListView( shrinkWrap: true, children: [ - PageTitle(name: S.of(context).nav_title(DrawerItem.navLibrary.title)), LibraryOccupationCard(), - if (occupation != null) PageTitle(name: S.of(context).floors), - if (occupation != null) getFloorRows(context, occupation!), + PageTitle(name: S.of(context).floors), + LazyConsumer( + builder: getFloorRows, + hasContent: (occupation) => occupation.floors.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_library_info, + style: const TextStyle(fontSize: 18), + ), + ), + contentLoadingWidget: const CircularProgressIndicator(), + ), ], ); } + // This will lazy consume Widget getFloorRows(BuildContext context, LibraryOccupation occupation) { final floors = []; for (var i = 1; i < occupation.floors.length; i += 2) { @@ -125,4 +116,14 @@ class LibraryPage extends StatelessWidget { ), ); } + + @override + Future onRefresh(BuildContext context) { + return Provider.of(context, listen: false) + .forceRefresh(context); + } + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navLibrary.route); } diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index ee6a4548b..d167efdf2 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -4,10 +4,8 @@ import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/library_occupation.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/utils/navigation_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. @@ -25,7 +23,7 @@ class LibraryOccupationCard extends GenericCard { @override Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${DrawerItem.navLibrary.title}'); + Navigator.pushNamed(context, '/${NavigationItem.navLibrary.route}'); @override void onRefresh(BuildContext context) { @@ -35,18 +33,13 @@ class LibraryOccupationCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( - builder: (context, libraryOccupationProvider) => - RequestDependentWidgetBuilder( - status: libraryOccupationProvider.status, - builder: () => generateOccupation( - libraryOccupationProvider.occupation, - context, - ), - hasContentPredicate: - libraryOccupationProvider.status != RequestStatus.busy, - onNullContent: const CircularProgressIndicator(), + return LazyConsumer( + builder: (context, libraryOccupation) => generateOccupation( + libraryOccupation, + context, ), + hasContent: (libraryOccupation) => true, + onNullContent: const CircularProgressIndicator(), ); } @@ -66,7 +59,7 @@ class LibraryOccupationCard extends GenericCard { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: CircularPercentIndicator( - radius: 60, + radius: 40, lineWidth: 8, percent: occupation.percentage / 100, center: Text( diff --git a/uni/lib/view/locale_notifier.dart b/uni/lib/view/locale_notifier.dart index 5ecbccda6..008508b49 100644 --- a/uni/lib/view/locale_notifier.dart +++ b/uni/lib/view/locale_notifier.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/model/entities/app_locale.dart'; class LocaleNotifier with ChangeNotifier { @@ -20,7 +20,7 @@ class LocaleNotifier with ChangeNotifier { void setLocale(AppLocale locale) { _locale = locale; - AppSharedPreferences.setLocale(locale); + PreferencesController.setLocale(locale); notifyListeners(); } diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index be9e1d15d..0d8dd55b7 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -1,12 +1,11 @@ +import 'package:diacritic/diacritic.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/location_group.dart'; import 'package:uni/model/providers/lazy/faculty_locations_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/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locations/widgets/faculty_map.dart'; @@ -17,66 +16,96 @@ class LocationsPage extends StatefulWidget { LocationsPageState createState() => LocationsPageState(); } -class LocationsPageState extends GeneralPageViewState +class LocationsPageState extends SecondaryPageViewState with SingleTickerProviderStateMixin { ScrollController? scrollViewController; - @override - void initState() { - super.initState(); - } - @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, locationsProvider) { + return LazyConsumer>( + builder: (context, locations) { return LocationsPageView( - locations: locationsProvider.locations, - status: locationsProvider.status, + locations: locations, ); }, + hasContent: (locations) => locations.isNotEmpty, + onNullContent: Center(child: Text(S.of(context).no_places_info)), ); } @override Future onRefresh(BuildContext context) async {} + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navLocations.route); } -class LocationsPageView extends StatelessWidget { +class LocationsPageView extends StatefulWidget { const LocationsPageView({ required this.locations, - required this.status, super.key, }); final List locations; - final RequestStatus status; + + @override + LocationsPageViewState createState() => LocationsPageViewState(); +} + +class LocationsPageViewState extends State { + static GlobalKey searchFormKey = GlobalKey(); + static String searchTerms = ''; @override Widget build(BuildContext context) { - return Column( - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.95, - padding: const EdgeInsets.fromLTRB(0, 0, 0, 4), - child: PageTitle( - name: '${S.of(context).nav_title(DrawerItem.navLocations.title)}:' - ' ${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: Center(child: Text(S.of(context).no_places_info)), - ), - // TODO(bdmendes): add support for multiple faculties - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + return Column( + children: [ + Container( + width: constraints.maxWidth - 40, + height: 40, + margin: const EdgeInsets.fromLTRB(20, 10, 20, 0), + child: TextFormField( + key: searchFormKey, + onChanged: (text) { + setState(() { + searchTerms = removeDiacritics(text.trim().toLowerCase()); + }); + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + contentPadding: const EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + ), + hintText: '${S.of(context).search}...', + ), + ), + ), + const SizedBox(height: 10), + Expanded( + child: Container( + height: 10, + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + alignment: Alignment.center, + child: FacultyMap( + faculty: getLocation(), + locations: widget.locations, + searchFilter: searchTerms, + interactiveFlags: + InteractiveFlag.all - InteractiveFlag.rotate, + // TODO(bdmendes): add support for multiple faculties + ), + ), + ), + const SizedBox( + height: 20, + ), + ], + ); + }, ); } diff --git a/uni/lib/view/locations/widgets/faculty_map.dart b/uni/lib/view/locations/widgets/faculty_map.dart index 1e2354e54..72364f8eb 100644 --- a/uni/lib/view/locations/widgets/faculty_map.dart +++ b/uni/lib/view/locations/widgets/faculty_map.dart @@ -4,9 +4,18 @@ import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/map.dart'; class FacultyMap extends StatelessWidget { - const FacultyMap({required this.faculty, required this.locations, super.key}); + const FacultyMap({ + required this.faculty, + required this.locations, + required this.searchFilter, + required this.interactiveFlags, + super.key, + }); + final String faculty; final List locations; + final String searchFilter; + final int interactiveFlags; @override Widget build(BuildContext context) { @@ -17,6 +26,8 @@ class FacultyMap extends StatelessWidget { southWestBoundary: const LatLng(41.17670, -8.59991), center: const LatLng(41.17731, -8.59522), locations: locations, + interactiveFlags: interactiveFlags, + searchFilter: searchFilter, ); default: return Container(); // Should not happen diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index c03b8b42c..254727a40 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:diacritic/diacritic.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; @@ -15,6 +16,8 @@ class LocationsMap extends StatelessWidget { required this.southWestBoundary, required this.center, required this.locations, + required this.interactiveFlags, + this.searchFilter = '', super.key, }); @@ -23,9 +26,25 @@ class LocationsMap extends StatelessWidget { final LatLng northEastBoundary; final LatLng southWestBoundary; final LatLng center; + final int interactiveFlags; + + final String searchFilter; @override Widget build(BuildContext context) { + final filteredLocations = List.from(locations); + if (searchFilter.trim().isNotEmpty) { + filteredLocations.retainWhere((location) { + final allLocations = location.floors.values.expand((x) => x); + return allLocations.any((location) { + return removeDiacritics(location.description().toLowerCase().trim()) + .contains( + searchFilter, + ); + }); + }); + } + return FlutterMap( options: MapOptions( minZoom: 17, @@ -34,7 +53,7 @@ class LocationsMap extends StatelessWidget { swPanBoundary: southWestBoundary, center: center, zoom: 17.5, - interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, + interactiveFlags: interactiveFlags, onTap: (tapPosition, latlng) => _popupLayerController.hideAllPopups(), ), nonRotatedChildren: [ @@ -65,7 +84,7 @@ class LocationsMap extends StatelessWidget { ), PopupMarkerLayer( options: PopupMarkerLayerOptions( - markers: locations.map((location) { + markers: filteredLocations.map((location) { return LocationMarker(location.latlng, location); }).toList(), popupController: _popupLayerController, diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 91a7251f6..1c46a3fd9 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -24,6 +24,7 @@ class LocationMarkerPopup extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(12), child: Wrap( + clipBehavior: Clip.antiAlias, direction: Axis.vertical, spacing: 8, children: (showId @@ -59,33 +60,37 @@ class Floor extends StatelessWidget { ? ' $floor' : '$floor'; - final Widget floorCol = Column( - mainAxisSize: MainAxisSize.min, + return Row( children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Text( + '${S.of(context).floor} $floorString', + style: TextStyle(color: fontColor), + ), + ), + ], + ), Container( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Text( - '${S.of(context).floor} $floorString', - style: TextStyle(color: fontColor), + decoration: + BoxDecoration(border: Border(left: BorderSide(color: fontColor))), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: locations + .map( + (location) => + LocationRow(location: location, color: fontColor), + ) + .toList(), ), ), ], ); - final Widget locationsColumn = Container( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - decoration: - BoxDecoration(border: Border(left: BorderSide(color: fontColor))), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: locations - .map( - (location) => LocationRow(location: location, color: fontColor), - ) - .toList(), - ), - ); - return Row(children: [floorCol, locationsColumn]); } } @@ -103,6 +108,7 @@ class LocationRow extends StatelessWidget { Text( location.description(), textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, style: TextStyle(color: color), ), ], diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 17d1076b7..39f44c081 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -9,7 +9,7 @@ import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/login_exceptions.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/providers/state_providers.dart'; -import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; import 'package:uni/view/home/widgets/exit_app_dialog.dart'; import 'package:uni/view/login/widgets/inputs.dart'; @@ -25,10 +25,6 @@ class LoginPageView extends StatefulWidget { /// Manages the 'login section' view. class LoginPageViewState extends State { - List faculties = [ - 'feup', - ]; // May choose more than one faculty in the dropdown. - static final FocusNode usernameFocus = FocusNode(); static final FocusNode passwordFocus = FocusNode(); @@ -58,13 +54,12 @@ class LoginPageViewState extends State { context, user, pass, - faculties, persistentSession: _keepSignedIn, ); if (context.mounted) { await Navigator.pushReplacementNamed( context, - '/${DrawerItem.navPersonalArea.title}', + '/${NavigationItem.navPersonalArea.route}', ); setState(() { _loggingIn = false; @@ -95,14 +90,6 @@ class LoginPageViewState extends State { } } - /// Updates the list of faculties - /// based on the options the user selected (used as a callback) - void setFaculties(List faculties) { - setState(() { - this.faculties = faculties; - }); - } - /// Tracks if the user wants to keep signed in (has a /// checkmark on the button). void _setKeepSignedIn({bool? value}) { @@ -215,7 +202,6 @@ class LoginPageViewState extends State { child: SingleChildScrollView( child: Column( children: [ - createFacultyInput(context, faculties, setFaculties), Padding( padding: EdgeInsets.only(bottom: queryData.size.height / 35), ), diff --git a/uni/lib/view/login/widgets/faculties_multiselect.dart b/uni/lib/view/login/widgets/faculties_multiselect.dart deleted file mode 100644 index 4188d558f..000000000 --- a/uni/lib/view/login/widgets/faculties_multiselect.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:uni/generated/l10n.dart'; -import 'package:uni/view/login/widgets/faculties_selection_form.dart'; - -class FacultiesMultiselect extends StatelessWidget { - const FacultiesMultiselect( - this.selectedFaculties, - this.setFaculties, { - super.key, - }); - final List selectedFaculties; - final void Function(List) setFaculties; - - @override - Widget build(BuildContext context) { - const textColor = Color.fromARGB(255, 0xfa, 0xfa, 0xfa); - - return TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w300, - color: textColor, - ), - ), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return FacultiesSelectionForm( - List.from(selectedFaculties), - setFaculties, - ); - }, - ); - }, - child: _createButtonContent(context), - ); - } - - Widget _createButtonContent(BuildContext context) { - return Container( - padding: const EdgeInsets.fromLTRB(5, 0, 5, 7), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.white, - ), - ), - ), - child: Row( - children: [ - Expanded( - child: Text( - _facultiesListText(context), - style: const TextStyle(color: Colors.white), - ), - ), - const Icon( - Icons.arrow_drop_down, - color: Colors.white, - ), - ], - ), - ); - } - - String _facultiesListText(BuildContext context) { - if (selectedFaculties.isEmpty) { - return S.of(context).no_college; - } - final buffer = StringBuffer(); - for (final faculty in selectedFaculties) { - buffer.write('${faculty.toUpperCase()}, '); - } - return buffer.toString().substring(0, buffer.length - 2); - } -} diff --git a/uni/lib/view/login/widgets/faculties_selection_form.dart b/uni/lib/view/login/widgets/faculties_selection_form.dart deleted file mode 100644 index 58602cca7..000000000 --- a/uni/lib/view/login/widgets/faculties_selection_form.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:uni/generated/l10n.dart'; -import 'package:uni/utils/constants.dart' as constants; -import 'package:uni/view/common_widgets/toast_message.dart'; - -class FacultiesSelectionForm extends StatefulWidget { - const FacultiesSelectionForm( - this.selectedFaculties, - this.setFaculties, { - super.key, - }); - final List selectedFaculties; - final void Function(List) setFaculties; - - @override - State createState() => _FacultiesSelectionFormState(); -} - -class _FacultiesSelectionFormState extends State { - @override - Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: const Color.fromARGB(255, 0x75, 0x17, 0x1e), - title: Text(S.of(context).college_select), - titleTextStyle: const TextStyle( - color: Color.fromARGB(255, 0xfa, 0xfa, 0xfa), - fontSize: 18, - ), - content: SizedBox( - height: 500, - width: 200, - child: createCheckList(context), - ), - actions: createActionButtons(context), - ); - } - - List createActionButtons(BuildContext context) { - return [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - S.of(context).cancel, - style: const TextStyle(color: Colors.white), - ), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, - backgroundColor: Colors.white, - ), - onPressed: () { - if (widget.selectedFaculties.isEmpty) { - ToastMessage.warning( - context, - S.of(context).at_least_one_college, - ); - return; - } - Navigator.pop(context); - widget.setFaculties(widget.selectedFaculties); - }, - child: Text(S.of(context).confirm), - ), - ]; - } - - Widget createCheckList(BuildContext context) { - return ListView( - children: List.generate(constants.faculties.length, (i) { - final faculty = constants.faculties.elementAt(i); - return CheckboxListTile( - title: Text( - faculty.toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 20), - ), - key: Key('FacultyCheck$faculty'), - value: widget.selectedFaculties.contains(faculty), - onChanged: (value) { - setState(() { - if (value != null && value) { - widget.selectedFaculties.add(faculty); - } else { - widget.selectedFaculties.remove(faculty); - } - }); - }, - ); - }), - ); - } -} diff --git a/uni/lib/view/login/widgets/inputs.dart b/uni/lib/view/login/widgets/inputs.dart index b926e1831..2f3d2b1f6 100644 --- a/uni/lib/view/login/widgets/inputs.dart +++ b/uni/lib/view/login/widgets/inputs.dart @@ -1,16 +1,6 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/view/about/widgets/terms_and_conditions.dart'; -import 'package:uni/view/login/widgets/faculties_multiselect.dart'; - -/// Creates the widget for the user to choose their faculty -Widget createFacultyInput( - BuildContext context, - List faculties, - void Function(List) setFaculties, -) { - return FacultiesMultiselect(faculties, setFaculties); -} /// Creates the widget for the username input. Widget createUsernameInput( diff --git a/uni/lib/view/navigation_service.dart b/uni/lib/view/navigation_service.dart index 903f5410e..e896d904d 100644 --- a/uni/lib/view/navigation_service.dart +++ b/uni/lib/view/navigation_service.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:uni/controller/cleanup.dart'; import 'package:uni/main.dart'; -import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/login/login.dart'; /// Manages the navigation logic @@ -13,9 +12,11 @@ class NavigationService { unawaited(cleanupStoredData(context)); - Navigator.pushNamedAndRemoveUntil( + Navigator.pushAndRemoveUntil( context, - '/${DrawerItem.navLogIn.title}', + MaterialPageRoute( + builder: (context) => const LoginPageView(), + ), (route) => false, ); } diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 6e23db12a..be6f22fdb 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -1,11 +1,14 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; +import 'package:uni/utils/navigation_items.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/print_info_card.dart'; import 'package:uni/view/profile/widgets/profile_overview.dart'; class ProfilePageView extends StatefulWidget { @@ -19,9 +22,8 @@ class ProfilePageView extends StatefulWidget { class ProfilePageViewState extends SecondaryPageViewState { @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, profileStateProvider) { - final profile = profileStateProvider.profile; + return LazyConsumer( + builder: (context, profile) { final courseWidgets = profile.courses .map( (e) => [ @@ -35,24 +37,30 @@ class ProfilePageViewState extends SecondaryPageViewState { return ListView( children: [ const Padding(padding: EdgeInsets.all(5)), - ProfileOverview( - profile: profile, - getProfileDecorationImage: getProfileDecorationImage, - ), + const Padding(padding: EdgeInsets.all(10)), + ProfileOverview(profile: profile), const Padding(padding: EdgeInsets.all(5)), - // TODO(bdmendes): Bring this back when print info is ready again - // PrintInfoCard() ...courseWidgets, AccountInfoCard(), + const Padding(padding: EdgeInsets.all(5)), + PrintInfoCard(), ], ); }, + hasContent: (Profile profile) => profile.courses.isNotEmpty, + onNullContent: Container(), ); } @override Widget getTopRightButton(BuildContext context) { - return Container(); + return IconButton( + icon: const Icon(Icons.settings), + onPressed: () => Navigator.pushNamed( + context, + '/${NavigationItem.navSettings.route}', + ), + ); } @override @@ -60,4 +68,9 @@ class ProfilePageViewState extends SecondaryPageViewState { return Provider.of(context, listen: false) .forceRefresh(context); } + + @override + String? getTitle() { + return null; + } } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 9e5162fa3..b2fe21eee 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/profile.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 /// through the top-right widget with the user picture) @@ -30,98 +30,78 @@ class AccountInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - 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)}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - Container( - margin: const EdgeInsets.only( - top: 20, - bottom: 8, - left: 20, - ), - child: Text( - S.of(context).balance, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - Container( - margin: const EdgeInsets.only( - top: 20, - bottom: 8, - right: 30, - ), - child: getInfoText(profile.feesBalance, context), - ), - ], + return Column( + children: [ + LazyConsumer( + builder: (BuildContext context, profile) => Table( + columnWidths: const {1: FractionColumnWidth(.4)}, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + Container( + margin: const EdgeInsets.only( + top: 20, + bottom: 8, + left: 20, ), - TableRow( - children: [ - Container( - margin: const EdgeInsets.only( - top: 8, - bottom: 20, - left: 20, - ), - child: Text( - S.of(context).fee_date, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - Container( - margin: const EdgeInsets.only( - top: 8, - bottom: 20, - right: 30, - ), - child: getInfoText( - profile.feesLimit != null - ? DateFormat('yyyy-MM-dd') - .format(profile.feesLimit!) - : S.of(context).no_date, - context, - ), - ), - ], + child: Text( + S.of(context).balance, + style: Theme.of(context).textTheme.titleSmall, ), - TableRow( - children: [ - Container( - margin: const EdgeInsets.only( - top: 8, - bottom: 20, - left: 20, - ), - child: Text( - S.of(context).fee_notification, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - Container( - margin: const EdgeInsets.only( - top: 8, - bottom: 20, - left: 20, - ), - child: const TuitionNotificationSwitch(), - ), - ], + ), + Container( + margin: const EdgeInsets.only( + top: 20, + bottom: 8, + right: 30, ), - ], - ), + child: getInfoText(profile.feesBalance, context), + ), + ], + ), + TableRow( + children: [ + Container( + margin: const EdgeInsets.only( + top: 8, + left: 20, + ), + child: Text( + S.of(context).fee_date, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Container( + margin: const EdgeInsets.only( + top: 8, + right: 30, + ), + child: getInfoText( + profile.feesLimit != null + ? DateFormat('yyyy-MM-dd').format(profile.feesLimit!) + : S.of(context).no_date, + context, + ), + ), + ], + ), + ], + ), + hasContent: (Profile profile) => true, + onNullContent: Container(), + ), + LazyConsumer>( + builder: (BuildContext context, references) { + return Column( + children: [ Container( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.only( + top: 30, + bottom: 10, + right: 15, + left: 15, + ), child: Row( children: [ Text( @@ -134,16 +114,14 @@ class AccountInfoCard extends GenericCard { ), ), ReferenceList(references: references), - const SizedBox(height: 10), - showLastRefreshedTime( - profileStateProvider.lastUpdateTime?.toIso8601String(), - context, - ), ], ); }, - ); - }, + hasContent: (references) => references.isNotEmpty, + onNullContent: Container(), + ), + const SizedBox(height: 10), + ], ); } 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 1e6eee537..7198b32a8 100644 --- a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart +++ b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart @@ -135,7 +135,7 @@ Future generateReference(BuildContext context, double amount) async { return; } - final session = Provider.of(context, listen: false).session; + final session = Provider.of(context, listen: false).state!; final response = await PrintFetcher.generatePrintMoneyReference(amount, session); diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index cebb17675..d99b93116 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/profile.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'; @@ -17,62 +18,45 @@ class PrintInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { - return LazyConsumer( - builder: (context, profileStateProvider) { - final profile = profileStateProvider.profile; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Table( - columnWidths: const { - 1: FractionColumnWidth(0.4), - 2: FractionColumnWidth(.1), - }, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - Container( - margin: const EdgeInsets.only( - top: 20, - bottom: 20, - left: 20, - ), - child: Text( - S.of(context).available_amount, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - Container( - margin: const EdgeInsets.only(right: 15), - child: Text( - profile.printBalance, - textAlign: TextAlign.end, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - Container( - margin: const EdgeInsets.only(right: 5), - height: 30, - child: ElevatedButton( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.zero, - ), - onPressed: () => addMoneyDialog(context), - child: const Center(child: Icon(Icons.add)), - ), - ), - ], - ), - ], - ), - showLastRefreshedTime( - profileStateProvider.lastUpdateTime?.toIso8601String(), - context, - ), - ], - ); - }, + return LazyConsumer( + builder: getPrintInfo, + hasContent: (profile) => profile.printBalance != '', + onNullContent: Center( + child: Text( + S.of(context).no_print_info, + style: const TextStyle(fontSize: 18), + ), + ), + ); + } + + Widget getPrintInfo(BuildContext context, Profile profile) { + return Row( + children: [ + Container( + margin: const EdgeInsets.only( + top: 20, + bottom: 20, + left: 20, + ), + child: Text( + S.of(context).available_amount, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + const Spacer(), + Expanded( + child: Text( + profile.printBalance, + textAlign: TextAlign.end, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + IconButton( + onPressed: () => addMoneyDialog(context), + icon: const Icon(Icons.add), + ), + ], ); } diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index 2ea9b1f30..51a6703cf 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -5,58 +5,57 @@ import 'package:provider/provider.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/view/common_widgets/widgets/profile_image.dart'; class ProfileOverview extends StatelessWidget { const ProfileOverview({ required this.profile, - required this.getProfileDecorationImage, super.key, }); + final Profile profile; - final DecorationImage Function(File?) getProfileDecorationImage; @override Widget build(BuildContext context) { - return Consumer( - builder: (context, sessionProvider, _) { - return FutureBuilder( - future: ProfileProvider.fetchOrGetCachedProfilePicture( - sessionProvider.session, + final session = context.read().state!; + return FutureBuilder( + future: ProfileProvider.fetchOrGetCachedProfilePicture( + session, + ), + builder: (BuildContext context, AsyncSnapshot profilePic) => + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const ProfileImage(radius: 75), + const Padding(padding: EdgeInsets.all(8)), + Text( + profile.name, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400, + ), + ), + const Padding(padding: EdgeInsets.all(5)), + Text( + profile.email, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w300, + ), ), - builder: (BuildContext context, AsyncSnapshot profilePic) => - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 150, - height: 150, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: getProfileDecorationImage(profilePic.data), - ), - ), - const Padding(padding: EdgeInsets.all(8)), - Text( - profile.name, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w400, - ), - ), - const Padding(padding: EdgeInsets.all(5)), - Text( - profile.email, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w300, - ), - ), - ], + const Padding(padding: EdgeInsets.all(5)), + Text( + session.faculties.map((e) => e.toUpperCase()).toList().join(', '), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w300, + ), ), - ); - }, + ], + ), ); } } diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 80a1c96b5..9a69298a4 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -5,10 +5,8 @@ import 'package:uni/model/entities/meal.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/utils/drawer_items.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/utils/navigation_items.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/locale_notifier.dart'; import 'package:uni/view/restaurant/widgets/restaurant_page_card.dart'; @@ -37,58 +35,56 @@ class _RestaurantPageViewState extends GeneralPageViewState scrollViewController = ScrollController(); } + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navRestaurants.route); + @override Widget getBody(BuildContext context) { - return LazyConsumer( - builder: (context, restaurantProvider) { - return Column( - children: [ - ListView( - shrinkWrap: true, - children: [ - Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), - alignment: Alignment.center, - child: PageTitle( - name: S - .of(context) - .nav_title(DrawerItem.navRestaurants.title), - 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: Center(child: Text(S.of(context).no_menus)), + return Column( + children: [ + TabBar( + controller: tabController, + isScrollable: true, + tabs: createTabs(context), + ), + LazyConsumer>( + builder: (context, restaurants) => createTabViewBuilder( + restaurants, + context, + ), + onNullContent: Center( + child: Text( + S.of(context).no_menus, + style: const TextStyle(fontSize: 18), ), - ], - ); - }, + ), + hasContent: (List restaurants) => restaurants.isNotEmpty, + ), + ], ); } - Widget createTabViewBuilder(dynamic restaurants, BuildContext context) { - final List dayContents = DayOfWeek.values.map((dayOfWeek) { - var restaurantsWidgets = []; - if (restaurants is List) { - restaurantsWidgets = restaurants - .map( - (restaurant) => createRestaurant(context, restaurant, dayOfWeek), - ) - .toList(); + Widget createTabViewBuilder( + List restaurants, + BuildContext context, + ) { + final dayContents = DayOfWeek.values.map((dayOfWeek) { + final restaurantsWidgets = restaurants + .where((element) => element.meals[dayOfWeek]?.isNotEmpty ?? false) + .map( + (restaurant) => createRestaurant(context, restaurant, dayOfWeek), + ) + .toList(); + if (restaurantsWidgets.isEmpty) { + return Center( + child: Text( + S.of(context).no_menus, + style: const TextStyle(fontSize: 18), + ), + ); } - return ListView(children: restaurantsWidgets); + return ListView(padding: EdgeInsets.zero, children: restaurantsWidgets); }).toList(); return Expanded( @@ -139,7 +135,7 @@ class _RestaurantPageViewState extends GeneralPageViewState final meals = restaurant.getMealsOfDay(day); if (meals.isEmpty) { return Container( - margin: const EdgeInsets.only(top: 10, bottom: 5), + margin: const EdgeInsets.only(bottom: 5), key: Key('restaurant-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index d02d616aa..cd6e7852a 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; +import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/restaurant.dart'; -import 'package:uni/model/providers/lazy/restaurant_provider.dart'; +import 'package:uni/utils/favorite_widget_type.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; -import 'package:uni/view/lazy_consumer.dart'; class RestaurantPageCard extends GenericCard { RestaurantPageCard(this.restaurant, this.meals, {super.key}) @@ -33,23 +34,88 @@ class RestaurantPageCard extends GenericCard { void onRefresh(BuildContext context) {} } -class CardFavoriteButton extends StatelessWidget { +class CardFavoriteButton extends StatefulWidget { const CardFavoriteButton(this.restaurant, {super.key}); + final Restaurant restaurant; + @override + State createState() { + return CardFavoriteButtonState(); + } +} + +class CardFavoriteButtonState extends State { + bool isFavorite = false; + + @override + void initState() { + super.initState(); + isFavorite = PreferencesController.getFavoriteRestaurants() + .contains(widget.restaurant.name); + } + @override Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, restaurantProvider) { - final isFavorite = - restaurantProvider.favoriteRestaurants.contains(restaurant.name); - return IconButton( - icon: isFavorite ? Icon(MdiIcons.heart) : Icon(MdiIcons.heartOutline), - onPressed: () => restaurantProvider.toggleFavoriteRestaurant( - restaurant.name, - ), - ); + return IconButton( + icon: isFavorite ? Icon(MdiIcons.heart) : Icon(MdiIcons.heartOutline), + onPressed: () async { + final favoriteRestaurants = + PreferencesController.getFavoriteRestaurants(); + if (favoriteRestaurants.contains(widget.restaurant.name)) { + favoriteRestaurants.remove(widget.restaurant.name); + } else { + favoriteRestaurants.add(widget.restaurant.name); + } + + setState(() { + PreferencesController.saveFavoriteRestaurants( + favoriteRestaurants, + ); + isFavorite = !isFavorite; + }); + + final favoriteCardTypes = PreferencesController.getFavoriteCards(); + if (context.mounted && + isFavorite && + !favoriteCardTypes.contains(FavoriteWidgetType.restaurant)) { + showRestaurantCardHomeDialog( + context, + favoriteCardTypes, + PreferencesController.saveFavoriteCards, + ); + } }, ); } + + void showRestaurantCardHomeDialog( + BuildContext context, + List favoriteCardTypes, + void Function(List) updateHomePage, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(S.of(context).restaurant_main_page), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(S.of(context).no), + ), + ElevatedButton( + onPressed: () { + updateHomePage( + favoriteCardTypes + [FavoriteWidgetType.restaurant], + ); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).yes), + ), + ], + ), + ); + } } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index 8ebe3664f..878d1e9b7 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -3,68 +3,59 @@ import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/lecture.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/model/utils/time/week.dart'; +import 'package:uni/model/utils/time/weekday_mapper.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/common_widgets/expanded_image_label.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/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locale_notifier.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class SchedulePage extends StatefulWidget { - const SchedulePage({super.key}); + SchedulePage({super.key, DateTime? now}) : now = now ?? DateTime.now(); + + final DateTime now; @override SchedulePageState createState() => SchedulePageState(); } -class SchedulePageState extends State { +class SchedulePageState extends SecondaryPageViewState { @override - Widget build(BuildContext context) { - return LazyConsumer( - builder: (context, lectureProvider) { - return SchedulePageView( - lectures: lectureProvider.lectures, - scheduleStatus: lectureProvider.status, - ); - }, + Widget getBody(BuildContext context) { + return LazyConsumer>( + builder: (context, lectures) => SchedulePageView( + lectures, + now: widget.now, + ), + hasContent: (lectures) => lectures.isNotEmpty, + onNullContent: SchedulePageView(const [], now: widget.now), ); } + + @override + Future onRefresh(BuildContext context) async { + await context.read().forceRefresh(context); + } + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navSchedule.route); } -/// Manages the 'schedule' sections of the app class SchedulePageView extends StatefulWidget { - SchedulePageView({ - required this.lectures, - required this.scheduleStatus, - super.key, - }); + SchedulePageView(this.lectures, {required DateTime now, super.key}) + : currentWeek = Week(start: now); final List lectures; - final RequestStatus scheduleStatus; - - final int weekDay = DateTime.now().weekday; - - static List> groupLecturesByDay(List schedule) { - final aggLectures = >[]; - - for (var i = 0; i < 5; i++) { - final lectures = {}; - for (var j = 0; j < schedule.length; j++) { - if (schedule[j].startTime.weekday - 1 == i) lectures.add(schedule[j]); - } - aggLectures.add(lectures); - } - return aggLectures; - } + final Week currentWeek; @override SchedulePageViewState createState() => SchedulePageViewState(); } -class SchedulePageViewState extends GeneralPageViewState +class SchedulePageViewState extends State with TickerProviderStateMixin { TabController? tabController; @@ -73,9 +64,11 @@ class SchedulePageViewState extends GeneralPageViewState super.initState(); tabController = TabController( vsync: this, - length: 5, + length: 6, ); - final offset = (widget.weekDay > 5) ? 0 : (widget.weekDay - 1) % 5; + + final weekDay = widget.currentWeek.start.weekday; + final offset = (weekDay > 6) ? 0 : (weekDay - 1) % 6; tabController?.animateTo(tabController!.index + offset); } @@ -86,35 +79,28 @@ class SchedulePageViewState extends GeneralPageViewState } @override - Widget getBody(BuildContext context) { + Widget build(BuildContext context) { final queryData = MediaQuery.of(context); - return Column( children: [ - ListView( - shrinkWrap: true, - children: [ - PageTitle( - name: S.of(context).nav_title( - DrawerItem.navSchedule.title, - ), - ), - TabBar( - controller: tabController, - isScrollable: true, - physics: const BouncingScrollPhysics(), - tabs: createTabs(queryData, context), - ), - ], + TabBar( + controller: tabController, + isScrollable: true, + physics: const BouncingScrollPhysics(), + tabs: createTabs(queryData, context), ), Expanded( child: TabBarView( controller: tabController, - children: createSchedule( - context, - widget.lectures, - widget.scheduleStatus, - ), + children: widget.currentWeek.weekdays.take(6).map((day) { + final lectures = lecturesOfDay(widget.lectures, day); + final index = WeekdayMapper.fromDartToIndex.map(day.weekday); + if (lectures.isEmpty) { + return emptyDayColumn(context, index); + } else { + return dayColumnBuilder(index, lectures, context); + } + }).toList(), ), ), ], @@ -124,9 +110,8 @@ class SchedulePageViewState extends GeneralPageViewState /// Returns a list of widgets empty with tabs for each day of the week. List createTabs(MediaQueryData queryData, BuildContext context) { final tabs = []; - final workWeekDays = Provider.of(context) - .getWeekdaysWithLocale() - .sublist(0, 5); + final workWeekDays = + context.read().getWeekdaysWithLocale().sublist(0, 6); workWeekDays.asMap().forEach((index, day) { tabs.add( SizedBox( @@ -141,81 +126,62 @@ class SchedulePageViewState extends GeneralPageViewState return tabs; } - List createSchedule( - BuildContext context, - List lectures, - RequestStatus scheduleStatus, - ) { - final tabBarViewContent = []; - for (var i = 0; i < 5; i++) { - tabBarViewContent - .add(createScheduleByDay(context, i, lectures, scheduleStatus)); - } - return tabBarViewContent; - } - - /// Returns a list of widgets for the rows with a singular class info. - List createScheduleRows(Set lectures, BuildContext context) { - final scheduleContent = []; - final lectureList = lectures.toList(); - for (var i = 0; i < lectureList.length; i++) { - final lecture = lectureList[i]; - scheduleContent.add( - ScheduleSlot( - subject: lecture.subject, - typeClass: lecture.typeClass, - rooms: lecture.room, - begin: lecture.startTime, - end: lecture.endTime, - occurrId: lecture.occurrId, - teacher: lecture.teacher, - classNumber: lecture.classNumber, - ), - ); - } - return scheduleContent; - } - Widget dayColumnBuilder( int day, - Set dayContent, + List lectures, BuildContext context, ) { return Container( key: Key('schedule-page-day-column-$day'), child: Column( mainAxisSize: MainAxisSize.min, - children: createScheduleRows(dayContent, context), + children: lectures + .map( + // TODO(thePeras): ScheduleSlot should receive a lecture + // instead of all these parameters. + (lecture) => ScheduleSlot( + subject: lecture.subject, + typeClass: lecture.typeClass, + rooms: lecture.room, + begin: lecture.startTime, + end: lecture.endTime, + occurrId: lecture.occurrId, + teacher: lecture.teacher, + classNumber: lecture.classNumber, + ), + ) + .toList(), ), ); } - Widget createScheduleByDay( - BuildContext context, - int day, - List lectures, - RequestStatus scheduleStatus, - ) { + static List lecturesOfDay(List lectures, DateTime day) { + final filteredLectures = []; + for (var i = 0; i < lectures.length; i++) { + final lecture = lectures[i]; + if (lecture.startTime.day == day.day && + lecture.startTime.month == day.month && + lecture.startTime.year == day.year) { + filteredLectures.add(lecture); + } + } + return filteredLectures; + } + + Widget emptyDayColumn(BuildContext context, int day) { final weekday = Provider.of(context).getWeekdaysWithLocale()[day]; - final aggLectures = SchedulePageView.groupLecturesByDay(lectures); - return RequestDependentWidgetBuilder( - status: scheduleStatus, - builder: () => dayColumnBuilder(day, aggLectures[day], context), - hasContentPredicate: aggLectures[day].isNotEmpty, - onNullContent: Center( - child: ImageLabel( - imagePath: 'assets/images/schedule.png', - label: '${S.of(context).no_classes_on} $weekday.', - labelTextStyle: const TextStyle(fontSize: 15), - ), + + final noClassesText = day >= DateTime.saturday - 1 + ? S.of(context).no_classes_on_weekend + : S.of(context).no_classes_on; + + return Center( + child: ImageLabel( + imagePath: 'assets/images/schedule.png', + label: '$noClassesText $weekday.', + 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 89f935bb2..48c55dcec 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -1,8 +1,12 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:uni/controller/networking/network_router.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/common_widgets/row_container.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/view/course_unit_info/course_unit_info.dart'; class ScheduleSlot extends StatelessWidget { const ScheduleSlot({ @@ -16,6 +20,7 @@ class ScheduleSlot extends StatelessWidget { this.classNumber, super.key, }); + final String subject; final String rooms; final DateTime begin; @@ -64,11 +69,12 @@ class ScheduleSlot extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium, alignment: TextAlign.center, ); - final roomTextField = TextFieldWidget( - text: rooms, + final roomTextField = Text( + rooms, style: Theme.of(context).textTheme.bodyMedium, - alignment: TextAlign.right, ); + final courseUnit = _correspondingCourseUnit(context); + return [ ScheduleTimeWidget( begin: DateFormat('HH:mm').format(begin), @@ -81,9 +87,10 @@ class ScheduleSlot extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SubjectButtonWidget( - occurrId: occurrId, - ), + if (courseUnit != null) + SubjectButtonWidget( + courseUnit: courseUnit, + ), subjectTextField, typeClassTextField, ], @@ -101,21 +108,27 @@ class ScheduleSlot extends StatelessWidget { roomTextField, ]; } + + CourseUnit? _correspondingCourseUnit(BuildContext context) { + final courseUnits = context.read().state!.courseUnits; + return courseUnits.firstWhereOrNull( + (courseUnit) => courseUnit.occurrId == occurrId, + ); + } } class SubjectButtonWidget extends StatelessWidget { - const SubjectButtonWidget({required this.occurrId, super.key}); - final int occurrId; + const SubjectButtonWidget({required this.courseUnit, super.key}); - String toUcLink(int occurrId) { - const faculty = 'feup'; // should not be hardcoded - return '${NetworkRouter.getBaseUrl(faculty)}' - 'UCURR_GERAL.FICHA_UC_VIEW?pv_ocorrencia_id=$occurrId'; - } + final CourseUnit courseUnit; - Future _launchURL() async { - final url = toUcLink(occurrId); - await launchUrl(Uri.parse(url)); + void _launchUcPage(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CourseUnitDetailPageView(courseUnit), + ), + ); } @override @@ -132,8 +145,8 @@ class SubjectButtonWidget extends StatelessWidget { iconSize: 18, color: Colors.grey, alignment: Alignment.centerRight, - tooltip: 'Abrir página da UC no browser', - onPressed: _launchURL, + tooltip: S.of(context).uc_info, + onPressed: () => _launchUcPage(context), ), ], ); @@ -146,6 +159,7 @@ class ScheduleTeacherClassInfoWidget extends StatelessWidget { this.classNumber, super.key, }); + final String? classNumber; final String teacher; @@ -161,6 +175,7 @@ class ScheduleTeacherClassInfoWidget extends StatelessWidget { class ScheduleTimeWidget extends StatelessWidget { const ScheduleTimeWidget({required this.begin, required this.end, super.key}); + final String begin; final String end; @@ -182,6 +197,7 @@ class ScheduleTimeTextField extends StatelessWidget { required this.context, super.key, }); + final String time; final BuildContext context; @@ -202,6 +218,7 @@ class TextFieldWidget extends StatelessWidget { required this.alignment, super.key, }); + final String text; final TextStyle? style; final TextAlign alignment; diff --git a/uni/lib/view/settings/settings.dart b/uni/lib/view/settings/settings.dart new file mode 100644 index 000000000..0354ea919 --- /dev/null +++ b/uni/lib/view/settings/settings.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/about/about.dart'; +import 'package:uni/view/bug_report/bug_report.dart'; +import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; +import 'package:uni/view/settings/widgets/locale_switch_button.dart'; +import 'package:uni/view/settings/widgets/logout_confirm_dialog.dart'; +import 'package:uni/view/settings/widgets/notifications_dialog.dart'; +import 'package:uni/view/settings/widgets/theme_switch_button.dart'; +import 'package:uni/view/settings/widgets/usage_stats_switch.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() { + return SettingsPageState(); + } +} + +class SettingsPageState extends SecondaryPageViewState { + @override + Widget getBody(BuildContext context) { + return Column( + children: [ + const Padding(padding: EdgeInsets.only(top: 50)), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView( + children: [ + ListTile( + title: Text(S.of(context).language), + trailing: const LocaleSwitchButton(), + ), + ListTile( + title: Text(S.of(context).theme), + trailing: const ThemeSwitchButton(), + ), + ListTile( + title: Text(S.of(context).collect_usage_stats), + trailing: const UsageStatsSwitch(), + ), + ListTile( + title: Text(S.of(context).notifications), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + showDialog( + context: context, + builder: (context) => const NotificationsDialog(), + ); + }, + ), + ListTile( + title: Text(S.of(context).report_error_suggestion), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BugReportPageView(), + ), + ); + }, + ), + ListTile( + title: Text(S.of(context).about), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AboutPageView(), + ), + ); + }, + ), + ListTile( + title: Text(S.of(context).logout), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () => showDialog( + context: context, + builder: (context) => const LogoutConfirmDialog(), + ), + ), + ], + ), + ), + ), + ], + ); + } + + @override + Future onRefresh(BuildContext context) async {} + + @override + String? getTitle() => S.of(context).settings; +} diff --git a/uni/lib/view/settings/widgets/locale_switch_button.dart b/uni/lib/view/settings/widgets/locale_switch_button.dart new file mode 100644 index 000000000..5b1963cbb --- /dev/null +++ b/uni/lib/view/settings/widgets/locale_switch_button.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/view/locale_notifier.dart'; + +class LocaleSwitchButton extends StatelessWidget { + const LocaleSwitchButton({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, localeNotifier, _) { + return ElevatedButton( + onPressed: () => localeNotifier.setNextLocale(), + child: Text( + localeNotifier.getLocale().localeCode.languageCode.toUpperCase(), + ), + ); + }, + ); + } +} diff --git a/uni/lib/view/settings/widgets/logout_confirm_dialog.dart b/uni/lib/view/settings/widgets/logout_confirm_dialog.dart new file mode 100644 index 000000000..2f632886f --- /dev/null +++ b/uni/lib/view/settings/widgets/logout_confirm_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/navigation_service.dart'; + +class LogoutConfirmDialog extends StatelessWidget { + const LogoutConfirmDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(S.of(context).logout), + content: Text(S.of(context).confirm_logout), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(S.of(context).no), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + NavigationService.logoutAndPopHistory(); + }, + child: Text(S.of(context).yes), + ), + ], + ); + } +} diff --git a/uni/lib/view/settings/widgets/notifications_dialog.dart b/uni/lib/view/settings/widgets/notifications_dialog.dart new file mode 100644 index 000000000..4defadfc2 --- /dev/null +++ b/uni/lib/view/settings/widgets/notifications_dialog.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/settings/widgets/tuition_notification_switch.dart'; + +class NotificationsDialog extends StatelessWidget { + const NotificationsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(S.of(context).notifications), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(S.of(context).fee_notification), + trailing: const TuitionNotificationSwitch(), + ), + ], + ), + ); + } +} diff --git a/uni/lib/view/settings/widgets/theme_switch_button.dart b/uni/lib/view/settings/widgets/theme_switch_button.dart new file mode 100644 index 000000000..21537c75c --- /dev/null +++ b/uni/lib/view/settings/widgets/theme_switch_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/view/theme_notifier.dart'; + +class ThemeSwitchButton extends StatelessWidget { + const ThemeSwitchButton({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, themeNotifier, _) { + final icon = switch (themeNotifier.getTheme()) { + ThemeMode.light => const Icon(Icons.wb_sunny), + ThemeMode.dark => const Icon(Icons.nightlight_round), + ThemeMode.system => const Icon(Icons.brightness_6), + }; + return ElevatedButton( + onPressed: themeNotifier.setNextTheme, + child: icon, + ); + }, + ); + } +} diff --git a/uni/lib/view/profile/widgets/tuition_notification_switch.dart b/uni/lib/view/settings/widgets/tuition_notification_switch.dart similarity index 58% rename from uni/lib/view/profile/widgets/tuition_notification_switch.dart rename to uni/lib/view/settings/widgets/tuition_notification_switch.dart index 9abd7766f..011de66c9 100644 --- a/uni/lib/view/profile/widgets/tuition_notification_switch.dart +++ b/uni/lib/view/settings/widgets/tuition_notification_switch.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; class TuitionNotificationSwitch extends StatefulWidget { const TuitionNotificationSwitch({super.key}); @@ -9,21 +9,11 @@ class TuitionNotificationSwitch extends StatefulWidget { } class _TuitionNotificationSwitchState extends State { - bool tuitionNotificationToggle = true; - - @override - void initState() { - super.initState(); - getTuitionNotificationToggle(); - } - - Future getTuitionNotificationToggle() async { - await AppSharedPreferences.getTuitionNotificationToggle() - .then((value) => setState(() => tuitionNotificationToggle = value)); - } + bool tuitionNotificationToggle = + PreferencesController.getTuitionNotificationToggle(); Future saveTuitionNotificationToggle({required bool value}) async { - await AppSharedPreferences.setTuitionNotificationToggle(value: value); + await PreferencesController.setTuitionNotificationToggle(value: value); setState(() { tuitionNotificationToggle = value; }); diff --git a/uni/lib/view/settings/widgets/usage_stats_switch.dart b/uni/lib/view/settings/widgets/usage_stats_switch.dart new file mode 100644 index 000000000..049f02cde --- /dev/null +++ b/uni/lib/view/settings/widgets/usage_stats_switch.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; + +class UsageStatsSwitch extends StatefulWidget { + const UsageStatsSwitch({super.key}); + + @override + State createState() => _UsageStatsSwitchState(); +} + +class _UsageStatsSwitchState extends State { + bool usageStatsToggle = PreferencesController.getUsageStatsToggle(); + + Future saveUsageStatsToggle({required bool value}) async { + await PreferencesController.setUsageStatsToggle(value: value); + setState(() { + usageStatsToggle = value; + }); + } + + @override + Widget build(BuildContext context) { + return Switch.adaptive( + value: usageStatsToggle, + onChanged: (value) => saveUsageStatsToggle(value: value), + ); + } +} diff --git a/uni/lib/view/terms_and_condition_dialog.dart b/uni/lib/view/terms_and_condition_dialog.dart index 5f4027ea0..50f72ccd6 100644 --- a/uni/lib/view/terms_and_condition_dialog.dart +++ b/uni/lib/view/terms_and_condition_dialog.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.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/fetchers/terms_and_conditions_fetcher.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/view/about/widgets/terms_and_conditions.dart'; enum TermsAndConditionsState { accepted, rejected } @@ -57,7 +57,7 @@ class TermsAndConditionDialog { Navigator.of(context).pop(); userTermsDecision .complete(TermsAndConditionsState.accepted); - await AppSharedPreferences + await PreferencesController .setTermsAndConditionsAcceptance(areAccepted: true); }, child: const Text( @@ -72,7 +72,7 @@ class TermsAndConditionDialog { Navigator.of(context).pop(); userTermsDecision .complete(TermsAndConditionsState.rejected); - await AppSharedPreferences + await PreferencesController .setTermsAndConditionsAcceptance(areAccepted: false); }, child: const Text( diff --git a/uni/lib/view/theme.dart b/uni/lib/view/theme.dart index 1a444ec77..c2eefca59 100644 --- a/uni/lib/view/theme.dart +++ b/uni/lib/view/theme.dart @@ -9,6 +9,7 @@ const Color _strongGrey = Color.fromARGB(255, 90, 90, 90); const Color _mildBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkishBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkBlack = Color.fromARGB(255, 27, 27, 27); +const Color _lightBlue = Color.fromARGB(255, 172, 193, 206); const _textTheme = TextTheme( displayLarge: TextStyle(fontSize: 40, fontWeight: FontWeight.w400), @@ -25,6 +26,7 @@ const _textTheme = TextTheme( ); ThemeData applicationLightTheme = ThemeData( + useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: darkRed, background: _mildWhite, @@ -35,16 +37,10 @@ ThemeData applicationLightTheme = ThemeData( tertiary: lightRed, onTertiary: Colors.black, ), - brightness: Brightness.light, primaryColor: darkRed, - textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, - ), - canvasColor: _mildWhite, - scaffoldBackgroundColor: _mildWhite, cardColor: Colors.white, - hintColor: _lightGrey, dividerColor: _lightGrey, + hintColor: _lightGrey, indicatorColor: darkRed, primaryTextTheme: Typography().black.copyWith( headlineMedium: const TextStyle(color: _strongGrey), @@ -52,72 +48,27 @@ ThemeData applicationLightTheme = ThemeData( ), iconTheme: const IconThemeData(color: darkRed), textTheme: _textTheme, - switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), - trackColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), - ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), - ), - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), - ), ); ThemeData applicationDarkTheme = ThemeData( + useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: lightRed, brightness: Brightness.dark, background: _darkBlack, primary: _lightGrey, onPrimary: _darkishBlack, - secondary: _lightGrey, + secondary: _lightBlue, onSecondary: _darkishBlack, tertiary: _lightGrey, onTertiary: _darkishBlack, ), - brightness: Brightness.dark, - textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, - ), primaryColor: _lightGrey, - canvasColor: _darkBlack, - scaffoldBackgroundColor: _darkBlack, cardColor: _mildBlack, - hintColor: _darkishBlack, dividerColor: _strongGrey, + hintColor: _darkishBlack, indicatorColor: _lightGrey, primaryTextTheme: Typography().white, iconTheme: const IconThemeData(color: _lightGrey), textTheme: _textTheme.apply(bodyColor: _lightGrey), - switchTheme: SwitchThemeData( - trackColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? _lightGrey : null, - ), - ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? _mildBlack : null, - ), - ), - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? _mildBlack : null, - ), - ), ); diff --git a/uni/lib/view/theme_notifier.dart b/uni/lib/view/theme_notifier.dart index 0ce1ffd5a..65374093a 100644 --- a/uni/lib/view/theme_notifier.dart +++ b/uni/lib/view/theme_notifier.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; class ThemeNotifier with ChangeNotifier { ThemeNotifier(this._themeMode); @@ -15,7 +15,7 @@ class ThemeNotifier with ChangeNotifier { void setTheme(ThemeMode themeMode) { _themeMode = themeMode; - AppSharedPreferences.setThemeMode(themeMode); + PreferencesController.setThemeMode(themeMode); notifyListeners(); } } diff --git a/uni/lib/view/transports/transports.dart b/uni/lib/view/transports/transports.dart new file mode 100644 index 000000000..fa51ab31b --- /dev/null +++ b/uni/lib/view/transports/transports.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; +import 'package:uni/view/home/widgets/bus_stop_card.dart'; +import 'package:uni/view/transports/widgets/map_snapshot_card.dart'; + +class TransportsPageView extends StatefulWidget { + const TransportsPageView({super.key}); + + @override + State createState() => TransportsPageViewState(); +} + +class TransportsPageViewState extends GeneralPageViewState { + List transportsCards = [ + MapCard(), + BusStopCard(), + // Add more cards if needed + ]; + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navTransports.route); + + @override + Widget getBody(BuildContext context) { + return ListView( + children: transportsCards, + ); + } + + @override + Future onRefresh(BuildContext context) async { + for (final card in transportsCards) { + card.onRefresh(context); + } + } +} diff --git a/uni/lib/view/transports/widgets/map_snapshot_card.dart b/uni/lib/view/transports/widgets/map_snapshot_card.dart new file mode 100644 index 000000000..5ba79c1f5 --- /dev/null +++ b/uni/lib/view/transports/widgets/map_snapshot_card.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/location_group.dart'; +import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/locations/widgets/faculty_map.dart'; + +class MapCard extends GenericCard { + MapCard({super.key}); + + const MapCard.fromEditingInformation( + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); + + @override + String getTitle(BuildContext context) => + '${S.of(context).nav_title(NavigationItem.navLocations.route)}: FEUP'; + + @override + Future onClick(BuildContext context) => + Navigator.pushNamed(context, '/${NavigationItem.navLocations.route}'); + + @override + Widget buildCardContent(BuildContext context) { + return LazyConsumer>( + builder: buildMapView, + hasContent: (locations) => locations.isNotEmpty, + onNullContent: const Center(child: Text('Erro')), + ); + } + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false) + .forceRefresh(context); + } + + Widget buildMapView(BuildContext context, List locations) { + return GestureDetector( + onTap: () => + Navigator.pushNamed(context, '/${NavigationItem.navLocations.route}'), + child: AbsorbPointer( + child: Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), + height: MediaQuery.of(context).size.height * 0.3, + alignment: Alignment.center, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: FacultyMap( + faculty: 'FEUP', + locations: locations, + interactiveFlags: InteractiveFlag.none, + searchFilter: '', + ), + ), + ), + ), + ); + } +} diff --git a/uni/lib/view/useful_info/useful_info.dart b/uni/lib/view/useful_info/useful_info.dart index 890b54cb8..8b1378917 100644 --- a/uni/lib/view/useful_info/useful_info.dart +++ b/uni/lib/view/useful_info/useful_info.dart @@ -1,50 +1 @@ -import 'package:flutter/material.dart'; -import 'package:uni/generated/l10n.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/useful_info/widgets/academic_services_card.dart'; -import 'package:uni/view/useful_info/widgets/copy_center_card.dart'; -import 'package:uni/view/useful_info/widgets/dona_bia_card.dart'; -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'; -class UsefulInfoPageView extends StatefulWidget { - const UsefulInfoPageView({super.key}); - - @override - State createState() => UsefulInfoPageViewState(); -} - -/// Manages the 'Useful Info' section of the app. -class UsefulInfoPageViewState extends GeneralPageViewState { - @override - Widget getBody(BuildContext context) { - return ListView( - children: [ - _getPageTitle(), - const AcademicServicesCard(), - const InfoDeskCard(), - const DonaBiaCard(), - const CopyCenterCard(), - const MultimediaCenterCard(), - const SigarraLinksCard(), - const OtherLinksCard(), - ], - ); - } - - Container _getPageTitle() { - return Container( - padding: const EdgeInsets.only(bottom: 6), - child: PageTitle( - name: S.of(context).nav_title(DrawerItem.navUsefulInfo.title), - ), - ); - } - - @override - Future onRefresh(BuildContext context) async {} -} diff --git a/uni/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/uni/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 4b4bb754d..000000000 --- a/uni/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/uni/pubspec.lock b/uni/pubspec.lock index 1ac709fa0..98b792ff1 100644 --- a/uni/pubspec.lock +++ b/uni/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" add_2_calendar: dependency: "direct main" description: name: add_2_calendar - sha256: dbcd0bf296fbbe00861a6f101af8cdb3c163a8c3ff5d3c99a4b081c2f37c724f + sha256: "8d7a82aba607d35f2a5bc913419e12f865a96a350a8ad2509a59322bc161f200" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "3.0.1" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" animated_stack_widget: dependency: transitive description: @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "06a96f1249f38a00435b3b0c9a3246d934d7dbc8183fc7c9e56989860edb99d4" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.4" + version: "3.4.10" args: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.2" async: dependency: transitive description: @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: ba605aeafd6609cb5f8020c609a51941803a5fb2b6a7576f7c7eeeb52d29e750 + url: "https://pub.dev" + source: hosted + version: "5.0.3" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: "942707f90e2f7481dcb178df02e22a9c6971b3562b848d6a1b8c7cff9f1a1fec" + url: "https://pub.dev" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -93,34 +109,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -133,34 +149,34 @@ packages: dependency: transitive description: name: built_value - sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.6.3" + version: "8.9.1" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" characters: dependency: transitive description: @@ -181,10 +197,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -197,26 +213,26 @@ packages: dependency: transitive description: name: code_builder - sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.10.0" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: "94d51c6f1299133a2baa4c5c3d2c11ec7d7fb4768dee5c52a56f7d7522fcf70e" + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -237,10 +253,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.2" crypto: dependency: "direct main" description: @@ -269,26 +285,34 @@ packages: dependency: "direct main" description: name: currency_text_input_formatter - sha256: "5d8db755ccde76817f6f5fb33e65304d7b1db442e1ccff079fe235584c6b7d5a" + sha256: b60c298fec9f0e96dfad88d25d026a6bf43f4e2bb9c59218afd8de1e09f54a60 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.11" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" + diacritic: + dependency: "direct main" + description: + name: diacritic + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" + url: "https://pub.dev" + source: hosted + version: "0.1.5" email_validator: dependency: "direct main" description: @@ -333,10 +357,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -351,7 +375,7 @@ packages: source: sdk version: "0.0.0" flutter_cache_manager: - dependency: transitive + dependency: "direct main" description: name: flutter_cache_manager sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" @@ -378,10 +402,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "501ed9d54f1c8c0535b7991bade36f9e7e3b45a2346401f03775c1ec7a3c06ae" + sha256: "401643a6ea9d8451365f2ec11145335bf130560cfde367bdf8f0be6d60f89479" url: "https://pub.dev" source: hosted - version: "15.1.2" + version: "15.1.3" flutter_local_notifications_linux: dependency: transitive description: @@ -423,18 +447,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "8afc9a6aa6d8e8063523192ba837149dbf3d377a37c0b0fc579149a1fbd4a619" + sha256: "5b24061317f850af858ef7151dadbb6eb77c1c449c954c7bb064e8a5e0e7d81f" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.20" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -449,10 +473,10 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - sha256: a7dc9eb4bfdef4ea06d114528bf52a7efcdfc6ba46d933957c25067f17c1c6b4 + sha256: "028f4989b9ff4907466af233d50146d807772600d98a3e895662fbdb09c39225" url: "https://pub.dev" source: hosted - version: "0.10.5" + version: "0.14.11" frontend_server_client: dependency: transitive description: @@ -489,10 +513,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -513,10 +537,10 @@ packages: dependency: "direct main" description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" intl: dependency: "direct main" description: @@ -585,10 +609,10 @@ packages: dependency: transitive description: name: markdown - sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.1" matcher: dependency: transitive description: @@ -617,10 +641,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mgrs_dart: dependency: transitive description: @@ -633,19 +657,18 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mockito: dependency: "direct dev" description: - path: "." - ref: e54a006 - resolved-ref: e54a00667cbe9a27de08e4c0ea355bacbe8c98d0 - url: "https://github.com/dart-lang/mockito.git" - source: git - version: "5.4.3-wip" + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" nested: dependency: transitive description: @@ -678,6 +701,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + open_file_plus: + dependency: "direct main" + description: + path: "." + ref: "3c32191" + resolved-ref: "3c321911c54388d1316e34d4f999776281398fc2" + url: "https://github.com/joutvhu/open_file_plus.git" + source: git + version: "3.4.1" package_config: dependency: transitive description: @@ -690,10 +722,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -722,26 +754,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -754,10 +786,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -778,34 +810,42 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" + plausible_analytics: + dependency: "direct main" + description: + name: plausible_analytics + sha256: be9f0b467d23cd94861737f10101431ad8b7d280dc0c14f7251e0e24655b07fa + url: "https://pub.dev" + source: hosted + version: "0.3.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" polylabel: dependency: transitive description: @@ -834,10 +874,10 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -866,26 +906,26 @@ packages: dependency: transitive description: name: sentry - sha256: "830667eadc0398fea3a3424ed1b74568e2db603a42758d0922e2f2974ce55a60" + sha256: d2ee9c850d876d285f22e2e662f400ec2438df9939fe4acd5d780df9841794ce url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.16.1" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "6730f41b304c6fb0fa590dacccaf73ba11082fc64b274cfe8a79776f2b95309c" + sha256: "5b428c189c825f16fb14e9166529043f06b965d5b59bfc3a1415e39c082398c0" url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.16.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_android: dependency: transitive description: @@ -898,42 +938,42 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shelf: dependency: transitive description: @@ -983,10 +1023,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_map_stack_trace: dependency: transitive description: @@ -1011,38 +1051,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + sqflite_common_ffi: + dependency: "direct dev" + description: + name: sqflite_common_ffi + sha256: "754927d82de369a6b9e760fb60640aa81da650f35ffd468d5a992814d6022908" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.3.2+1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1063,10 +1127,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1079,26 +1143,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" timelines: dependency: "direct main" description: @@ -1147,102 +1211,118 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.dev" + source: hosted + version: "0.7.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.2.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1263,10 +1343,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -1279,10 +1359,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -1303,10 +1383,10 @@ packages: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.2.0" wkt_parser: dependency: transitive description: @@ -1327,18 +1407,18 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -1348,5 +1428,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.2 <4.0.0" - flutter: ">=3.13.7" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index ddc92328c..664f629a7 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -7,25 +7,29 @@ publish_to: "none" # We do not publish to pub.dev # To change it manually, override the value in app_version.txt. # The app version code is automatically also bumped by CI. # Do not change it manually. -version: 1.7.22+235 +version: 1.8.0-beta.19+252 + environment: sdk: ">=3.0.0 <4.0.0" flutter: 3.13.7 dependencies: - add_2_calendar: ^2.2.5 - cached_network_image: ^3.2.3 + add_2_calendar: ^3.0.1 + battery_plus: ^5.0.2 + cached_network_image: ^3.3.1 collection: ^1.16.0 - connectivity_plus: ^5.0.0 + connectivity_plus: ^5.0.2 crypto: ^3.0.1 cupertino_icons: ^1.0.2 currency_text_input_formatter: ^2.1.5 + diacritic: ^0.1.5 email_validator: ^2.0.1 encrypt: ^5.0.3 expansion_tile_card: ^3.0.0 flutter: sdk: flutter + flutter_cache_manager: ^3.3.1 flutter_dotenv: ^5.0.2 flutter_local_notifications: ^15.1.0+1 flutter_localizations: @@ -33,48 +37,50 @@ dependencies: flutter_map: ^5.0.0 flutter_map_marker_popup: ^5.0.0 flutter_markdown: ^0.6.0 - flutter_svg: ^2.0.0+1 - flutter_widget_from_html_core: ^0.10.3 + flutter_svg: ^2.0.9 + flutter_widget_from_html_core: ^0.14.11 html: ^0.15.0 http: ^1.1.0 - image: ^4.0.13 + image: ^4.1.4 intl: ^0.18.0 latlong2: ^0.9.0 logger: ^2.0.2+1 material_design_icons_flutter: ^7.0.7296 + open_file_plus: + git: + url: https://github.com/joutvhu/open_file_plus.git + ref: "3c32191" path: ^1.8.0 - path_provider: ^2.0.0 + path_provider: ^2.1.2 percent_indicator: ^4.2.2 - provider: ^6.0.4 - sentry_flutter: ^7.9.0 - shared_preferences: ^2.0.3 + plausible_analytics: ^0.3.0 + provider: ^6.1.1 + sentry_flutter: ^7.14.0 + shared_preferences: ^2.2.2 shimmer: ^3.0.0 sqflite: ^2.0.3 synchronized: ^3.0.0 timelines: ^0.1.0 tuple: ^2.0.0 - url_launcher: ^6.0.2 + url_launcher: ^6.2.2 workmanager: ^0.5.2 dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.8 flutter_launcher_icons: ^0.13.1 flutter_test: sdk: flutter - # FIXME(luisd): Update mockito to a release version when 5.4.3 or above - # is launched - mockito: - git: - url: https://github.com/dart-lang/mockito.git - ref: "e54a006" + mockito: 5.4.4 + sqflite_common_ffi: ^2.3.0+4 test: any very_good_analysis: ^5.1.0 + flutter: generate: true uses-material-design: true assets: - - assets/ + - assets/env/ - assets/images/ - assets/text/ - assets/text/locations/ diff --git a/uni/test/integration/resources/exam2_example.html b/uni/test/integration/resources/exam2_example.html new file mode 100644 index 000000000..48f48badb --- /dev/null +++ b/uni/test/integration/resources/exam2_example.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + +FCUP - Mapa de Exames + + + + + + + + + + + +
+ +
Voc est em: Incio > Cursos/CE > Chamadas > Exames
+
+
+ + +
+
+Mapa das Instalaes +
+
+
+FC6 - Departamento de Cincia de Computadores +FC5 - Edifcio Central +FC4 - Departamento de Biologia +FC3 - Departamento de Fsica e Astronomia e Departamento GAOT +FC2 - Departamento de Qumica e Bioqumica +FC1 - Departamento de Matemtica + +
+
+
+ +
+
+
+
Atalhos
+
+ + + +
+ +
+
Opes
+ +
+
+
+

Cursos

+ +

Mapa de Exames

+

L:EF

+

Normal - poca Normal (2S)

+ + +
+ + + + + + + + + + + + + + + + + +
Segunda
2099-05-29
Tera
2099-05-30
Quarta
2099-05-31
Quinta
2099-06-01
Sexta
2099-06-02
Sbado
2099-06-03

+ + + + +
FIS2019
0:00-

+
+

+ + + + + + + + + + + + + + + + + +
Segunda
2099-06-05
Tera
2099-06-06
Quarta
2099-06-07
Quinta
2099-06-08
Sexta
2099-06-09
Sbado
2099-06-10
+ + + + +
FIS3020
0:00-

+
+ + + + +
Q1004
0:00-

+
+ + + + +
FIS2017
0:00-

+

+ + + + + + + +
FIS3022
0:00-

CCINF1002
0:00-

+

+

+ + + + + + + + + + + + + + + + + +
Segunda
2099-06-12
Tera
2099-06-13
Quarta
2099-06-14
Quinta
2099-06-15
Sexta
2099-06-16
Sbado
2099-06-17
+ + + + +
FIS2018
0:00-

+
+ + + + +
FIS1014
0:00-

+

+ + + + +
M2030
09:30-12:30

FC1122, FC1219
+
+

+ + + + + + + + + + + + + + + + +
Segunda
2099-06-19
Tera
2099-06-20
Quarta
2099-06-21
Quinta
2099-06-22
Sexta
2099-06-23
Sbado
2099-06-24
+ + + + +
M1015
09:00-12:00

FC1120, FC1122, FC1219, FC1226
+

+

+

Recurso - poca Recurso (2S)

+ + +
+ + + + + + + + + + + + + + + + + +
Segunda
2099-06-26
Tera
2099-06-27
Quarta
2099-06-28
Quinta
2099-06-29
Sexta
2099-06-30
Sbado
2099-07-01
+ + + + + + + +
CCINF1002
0:00-

FIS2019
0:00-

+
+ + + + +
FIS3022
0:00-

+
+ + + + +
FIS1018
0:00-

+

+ + + + + + + +
M2030
0:00-

M1015
0:00-

+

+

+ + + + + + + + + + + + + + + + +
Segunda
2099-07-03
Tera
2099-07-04
Quarta
2099-07-05
Quinta
2099-07-06
Sexta
2099-07-07
Sbado
2099-07-08

+ + + + +
FIS1014
0:00-

+
+ + + + +
FIS2018
0:00-

+
+ + + + +
FIS3020
0:00-

+
+ + + + + + + +
FIS2017
0:00-

Q1004
0:00-

+

+

+
+
+
+
+ +Recomendar Pgina + + +Voltar ao Topo
+
+ +Copyright 1996-2099 © Faculdade de Cincias da Universidade do Porto + I Termos e Condies + I Acessibilidade + I ndice A-Z + I Livro de Visitas +
+Pgina gerada em: 2099-04-28 s 16:13:57 + +| Poltica de Utilizao Aceitvel | Poltica de Proteo de Dados Pessoais | Denncias +

+
+ + + + + + + diff --git a/uni/test/integration/src/exams2_page_test.dart b/uni/test/integration/src/exams2_page_test.dart new file mode 100644 index 000000000..c14fbec0d --- /dev/null +++ b/uni/test/integration/src/exams2_page_test.dart @@ -0,0 +1,119 @@ +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; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.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_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/lazy/exam_provider.dart'; +import 'package:uni/view/exams/exams.dart'; + +import '../../mocks/integration/src/exams_page_test.mocks.dart'; +import '../../test_widget.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() async { + await initTestEnvironment(); + + group('ExamsPage Second Integration Tests', () { + final mockClient = MockClient(); + final mockResponse = MockResponse(); + final fis3022CourseUnit = CourseUnit( + abbreviation: 'FIS3022', + name: 'Métodos Computacionais em Engenharia', + occurrId: 0, + status: 'V', + ); + final m2030CourseUnit = CourseUnit( + abbreviation: 'M2030', + name: 'Probabilidade e Estatística', + occurrId: 0, + status: 'V', + ); + + final beginFis3022Exam = DateTime.parse('2099-06-09 00:00'); + final endFis3022Exam = DateTime.parse('2099-06-09 00:00'); + final fis3022Exam = Exam( + '33999', + beginFis3022Exam, + endFis3022Exam, + 'FIS3022', + [], + 'EN', + 'fcup', + ); + final beginM2030Exam = DateTime.parse('2099-06-17 09:30'); + final endM2030Exam = DateTime.parse('2099-06-17 12:30'); + final m2030Exam = Exam( + '34053', + beginM2030Exam, + endM2030Exam, + 'M2030', + ['FC1122', 'FC1219'], + 'EN', + 'fcup', + ); + final beginQ1004Exam = DateTime.parse('2099-06-07 00:00'); + final endQ1004Exam = DateTime.parse('2099-06-07 00:00'); + final q1004Exam = + Exam('34085', beginQ1004Exam, endQ1004Exam, 'Q1004', [], 'EN', 'fcup'); + + final filteredExams = {}; + for (final type in Exam.displayedTypes) { + filteredExams[type] = true; + } + + final profile = Profile()..courses = [Course(id: 9113, faculty: 'fcup')]; + + testWidgets('Exams', (WidgetTester tester) async { + NetworkRouter.httpClient = mockClient; + final mockHtml = File('test/integration/resources/exam2_example.html') + .readAsStringSync(encoding: latin1); + when(mockResponse.body).thenReturn(mockHtml); + when(mockResponse.statusCode).thenReturn(200); + when(mockClient.get(any, headers: anyNamed('headers'))) + .thenAnswer((_) async => mockResponse); + + final examProvider = ExamProvider(); + + const widget = ExamsPageView(); + + final providers = [ + ChangeNotifierProvider(create: (_) => examProvider), + ]; + await tester.pumpWidget(testableWidget(widget, providers: providers)); + + expect(find.byKey(Key('$fis3022Exam-exam')), findsNothing); + expect(find.byKey(Key('$m2030Exam-exam')), findsNothing); + expect(find.byKey(Key('$q1004Exam-exam')), findsNothing); + + final exams = await examProvider.fetchUserExams( + ParserExams(), + profile, + Session(username: '', cookies: '', faculties: ['fcup']), + [fis3022CourseUnit, m2030CourseUnit], + persistentSession: false, + ); + + examProvider.setState(exams); + + expect(examProvider.state!.contains(fis3022Exam), true); + expect(examProvider.state!.contains(m2030Exam), true); + + await tester.pumpAndSettle(); + + expect(find.byKey(Key('$fis3022Exam-exam')), findsOneWidget); + expect(find.byKey(Key('$m2030Exam-exam')), findsOneWidget); + expect(find.byKey(Key('$q1004Exam-exam')), findsNothing); + }); + }); +} diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index 4e37ebd3f..c0bb1160b 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -20,7 +20,9 @@ import '../../mocks/integration/src/exams_page_test.mocks.dart'; import '../../test_widget.dart'; @GenerateNiceMocks([MockSpec(), MockSpec()]) -void main() { +void main() async { + await initTestEnvironment(); + group('ExamsPage Integration Tests', () { final mockClient = MockClient(); final mockResponse = MockResponse(); @@ -79,7 +81,7 @@ void main() { expect(find.byKey(Key('$sopeExam-exam')), findsNothing); expect(find.byKey(Key('$mdisExam-exam')), findsNothing); - await examProvider.fetchUserExams( + final exams = await examProvider.fetchUserExams( ParserExams(), profile, Session(username: '', cookies: '', faculties: ['feup']), @@ -87,7 +89,7 @@ void main() { persistentSession: false, ); - examProvider.markAsInitialized(); + examProvider.setState(exams); await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); @@ -117,7 +119,7 @@ void main() { expect(find.byKey(Key('$sdisExam-exam')), findsNothing); expect(find.byKey(Key('$sopeExam-exam')), findsNothing); - await examProvider.fetchUserExams( + final exams = await examProvider.fetchUserExams( ParserExams(), profile, Session(username: '', cookies: '', faculties: ['feup']), @@ -125,20 +127,18 @@ void main() { persistentSession: false, ); - examProvider.markAsInitialized(); + examProvider.setState(exams); await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); expect(find.byKey(Key('$sopeExam-exam')), findsOneWidget); - expect(find.byIcon(Icons.filter_alt), findsOneWidget); + expect(find.byIcon(Icons.filter_list), findsOneWidget); filteredExams['ExamDoesNotExist'] = true; - await examProvider.setFilteredExams(filteredExams); - await tester.pumpAndSettle(); - final filterButton = find.widgetWithIcon(IconButton, Icons.filter_alt); + final filterButton = find.widgetWithIcon(IconButton, Icons.filter_list); expect(filterButton, findsOneWidget); await tester.tap(filterButton); diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index bfa07b2a2..07a7c8a2e 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -31,7 +31,9 @@ class UriMatcher extends CustomMatcher { MockSpec(), MockSpec(), ]) -void main() { +void main() async { + await initTestEnvironment(); + group('SchedulePage Integration Tests', () { final mockClient = MockClient(); final mockResponse = MockResponse(); @@ -49,11 +51,11 @@ void main() { final scheduleProvider = LectureProvider(); final sessionProvider = MockSessionProvider(); - when(sessionProvider.session).thenReturn( + when(sessionProvider.state).thenReturn( Session(username: 'up1234', cookies: 'cookie', faculties: ['feup']), ); - const widget = SchedulePage(); + final widget = SchedulePage(); final providers = [ ChangeNotifierProvider(create: (_) => scheduleProvider), @@ -69,25 +71,32 @@ void main() { expect(find.byKey(const Key(scheduleSlotTimeKey1)), findsNothing); expect(find.byKey(const Key(scheduleSlotTimeKey2)), findsNothing); - await scheduleProvider.fetchUserLectures( + final lectures = await scheduleProvider.fetchUserLectures( Session(username: '', cookies: '', faculties: ['feup']), profile, persistentSession: false, ); - scheduleProvider.markAsInitialized(); + scheduleProvider.setState(lectures); + + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('schedule-page-tab-2'))); await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('schedule-page-tab-1'))); await tester.tap(find.byKey(const Key('schedule-page-tab-1'))); await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('schedule-page-tab-0'))); 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.ensureVisible(find.byKey(const Key('schedule-page-tab-2'))); await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('schedule-page-tab-3'))); await tester.tap(find.byKey(const Key('schedule-page-tab-3'))); await tester.pumpAndSettle(); diff --git a/uni/test/mocks/integration/src/exams2_page_test.mocks.dart b/uni/test/mocks/integration/src/exams2_page_test.mocks.dart new file mode 100644 index 000000000..2720f3dd6 --- /dev/null +++ b/uni/test/mocks/integration/src/exams2_page_test.mocks.dart @@ -0,0 +1,420 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in uni/test/integration/src/exams2_page_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + @override + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + returnValueForMissingStub: + _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Response]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockResponse extends _i1.Mock implements _i2.Response { + @override + _i6.Uint8List get bodyBytes => (super.noSuchMethod( + Invocation.getter(#bodyBytes), + returnValue: _i6.Uint8List(0), + returnValueForMissingStub: _i6.Uint8List(0), + ) as _i6.Uint8List); + + @override + String get body => (super.noSuchMethod( + Invocation.getter(#body), + returnValue: _i5.dummyValue( + this, + Invocation.getter(#body), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.getter(#body), + ), + ) as String); + + @override + int get statusCode => (super.noSuchMethod( + Invocation.getter(#statusCode), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + + @override + Map get headers => (super.noSuchMethod( + Invocation.getter(#headers), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + + @override + bool get isRedirect => (super.noSuchMethod( + Invocation.getter(#isRedirect), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get persistentConnection => (super.noSuchMethod( + Invocation.getter(#persistentConnection), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); +} diff --git a/uni/test/mocks/integration/src/exams_page_test.mocks.dart b/uni/test/mocks/integration/src/exams_page_test.mocks.dart index e28b49f59..2d6896d46 100644 --- a/uni/test/mocks/integration/src/exams_page_test.mocks.dart +++ b/uni/test/mocks/integration/src/exams_page_test.mocks.dart @@ -1,14 +1,15 @@ -// Mocks generated by Mockito 5.4.3-wip from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in uni/test/integration/src/exams_page_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'dart:convert' as _i4; -import 'dart:typed_data' as _i5; +import 'dart:typed_data' as _i6; import 'package:http/http.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -77,6 +78,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> get( Uri? url, { @@ -106,6 +108,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> post( Uri? url, { @@ -149,6 +152,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> put( Uri? url, { @@ -192,6 +196,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> patch( Uri? url, { @@ -235,6 +240,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> delete( Uri? url, { @@ -278,6 +284,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future read( Uri? url, { @@ -289,11 +296,27 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i3.Future.value(''), - returnValueForMissingStub: _i3.Future.value(''), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), ) as _i3.Future); + @override - _i3.Future<_i5.Uint8List> readBytes( + _i3.Future<_i6.Uint8List> readBytes( Uri? url, { Map? headers, }) => @@ -303,10 +326,11 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), returnValueForMissingStub: - _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - ) as _i3.Future<_i5.Uint8List>); + _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + @override _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( @@ -331,6 +355,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.StreamedResponse>); + @override void close() => super.noSuchMethod( Invocation.method( @@ -346,35 +371,46 @@ class MockClient extends _i1.Mock implements _i2.Client { /// See the documentation for Mockito's code generation for more information. class MockResponse extends _i1.Mock implements _i2.Response { @override - _i5.Uint8List get bodyBytes => (super.noSuchMethod( + _i6.Uint8List get bodyBytes => (super.noSuchMethod( Invocation.getter(#bodyBytes), - returnValue: _i5.Uint8List(0), - returnValueForMissingStub: _i5.Uint8List(0), - ) as _i5.Uint8List); + returnValue: _i6.Uint8List(0), + returnValueForMissingStub: _i6.Uint8List(0), + ) as _i6.Uint8List); + @override String get body => (super.noSuchMethod( Invocation.getter(#body), - returnValue: '', - returnValueForMissingStub: '', + returnValue: _i5.dummyValue( + this, + Invocation.getter(#body), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.getter(#body), + ), ) as String); + @override int get statusCode => (super.noSuchMethod( Invocation.getter(#statusCode), returnValue: 0, returnValueForMissingStub: 0, ) as int); + @override Map get headers => (super.noSuchMethod( Invocation.getter(#headers), returnValue: {}, returnValueForMissingStub: {}, ) as Map); + @override bool get isRedirect => (super.noSuchMethod( Invocation.getter(#isRedirect), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override bool get persistentConnection => (super.noSuchMethod( Invocation.getter(#persistentConnection), diff --git a/uni/test/mocks/integration/src/schedule_page_test.mocks.dart b/uni/test/mocks/integration/src/schedule_page_test.mocks.dart index e5e3b4548..65e5988a5 100644 --- a/uni/test/mocks/integration/src/schedule_page_test.mocks.dart +++ b/uni/test/mocks/integration/src/schedule_page_test.mocks.dart @@ -1,20 +1,21 @@ -// Mocks generated by Mockito 5.4.3-wip from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in uni/test/integration/src/schedule_page_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; import 'dart:convert' as _i5; -import 'dart:typed_data' as _i6; -import 'dart:ui' as _i11; +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i12; -import 'package:flutter/material.dart' as _i10; +import 'package:flutter/material.dart' as _i11; import 'package:http/http.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:uni/model/entities/profile.dart' as _i9; +import 'package:mockito/src/dummies.dart' as _i6; import 'package:uni/model/entities/session.dart' as _i3; -import 'package:uni/model/providers/startup/session_provider.dart' as _i7; -import 'package:uni/model/request_status.dart' as _i8; +import 'package:uni/model/providers/startup/session_provider.dart' as _i8; +import 'package:uni/model/providers/state_providers.dart' as _i10; +import 'package:uni/model/request_status.dart' as _i9; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -93,6 +94,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future<_i2.Response> get( Uri? url, { @@ -122,6 +124,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future<_i2.Response> post( Uri? url, { @@ -165,6 +168,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future<_i2.Response> put( Uri? url, { @@ -208,6 +212,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future<_i2.Response> patch( Uri? url, { @@ -251,6 +256,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future<_i2.Response> delete( Uri? url, { @@ -294,6 +300,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.Response>); + @override _i4.Future read( Uri? url, { @@ -305,11 +312,27 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future.value(''), - returnValueForMissingStub: _i4.Future.value(''), + returnValue: _i4.Future.value(_i6.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i4.Future.value(_i6.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), ) as _i4.Future); + @override - _i4.Future<_i6.Uint8List> readBytes( + _i4.Future<_i7.Uint8List> readBytes( Uri? url, { Map? headers, }) => @@ -319,10 +342,11 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + returnValue: _i4.Future<_i7.Uint8List>.value(_i7.Uint8List(0)), returnValueForMissingStub: - _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), - ) as _i4.Future<_i6.Uint8List>); + _i4.Future<_i7.Uint8List>.value(_i7.Uint8List(0)), + ) as _i4.Future<_i7.Uint8List>); + @override _i4.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( @@ -347,6 +371,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i4.Future<_i2.StreamedResponse>); + @override void close() => super.noSuchMethod( Invocation.method( @@ -362,35 +387,46 @@ class MockClient extends _i1.Mock implements _i2.Client { /// See the documentation for Mockito's code generation for more information. class MockResponse extends _i1.Mock implements _i2.Response { @override - _i6.Uint8List get bodyBytes => (super.noSuchMethod( + _i7.Uint8List get bodyBytes => (super.noSuchMethod( Invocation.getter(#bodyBytes), - returnValue: _i6.Uint8List(0), - returnValueForMissingStub: _i6.Uint8List(0), - ) as _i6.Uint8List); + returnValue: _i7.Uint8List(0), + returnValueForMissingStub: _i7.Uint8List(0), + ) as _i7.Uint8List); + @override String get body => (super.noSuchMethod( Invocation.getter(#body), - returnValue: '', - returnValueForMissingStub: '', + returnValue: _i6.dummyValue( + this, + Invocation.getter(#body), + ), + returnValueForMissingStub: _i6.dummyValue( + this, + Invocation.getter(#body), + ), ) as String); + @override int get statusCode => (super.noSuchMethod( Invocation.getter(#statusCode), returnValue: 0, returnValueForMissingStub: 0, ) as int); + @override Map get headers => (super.noSuchMethod( Invocation.getter(#headers), returnValue: {}, returnValueForMissingStub: {}, ) as Map); + @override bool get isRedirect => (super.noSuchMethod( Invocation.getter(#isRedirect), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override bool get persistentConnection => (super.noSuchMethod( Invocation.getter(#persistentConnection), @@ -402,25 +438,14 @@ class MockResponse extends _i1.Mock implements _i2.Response { /// A class which mocks [SessionProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { - @override - _i3.Session get session => (super.noSuchMethod( - Invocation.getter(#session), - returnValue: _FakeSession_2( - this, - Invocation.getter(#session), - ), - returnValueForMissingStub: _FakeSession_2( - this, - Invocation.getter(#session), - ), - ) as _i3.Session); +class MockSessionProvider extends _i1.Mock implements _i8.SessionProvider { @override bool get dependsOnSession => (super.noSuchMethod( Invocation.getter(#dependsOnSession), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override set dependsOnSession(bool? _dependsOnSession) => super.noSuchMethod( Invocation.setter( @@ -429,6 +454,7 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { ), returnValueForMissingStub: null, ); + @override set cacheDuration(Duration? _cacheDuration) => super.noSuchMethod( Invocation.setter( @@ -437,66 +463,73 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { ), returnValueForMissingStub: null, ); + @override - _i8.RequestStatus get status => (super.noSuchMethod( - Invocation.getter(#status), - returnValue: _i8.RequestStatus.none, - returnValueForMissingStub: _i8.RequestStatus.none, - ) as _i8.RequestStatus); + _i9.RequestStatus get requestStatus => (super.noSuchMethod( + Invocation.getter(#requestStatus), + returnValue: _i9.RequestStatus.none, + returnValueForMissingStub: _i9.RequestStatus.none, + ) as _i9.RequestStatus); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override - _i4.Future loadFromStorage() => (super.noSuchMethod( + _i4.Future<_i3.Session> loadFromStorage( + _i10.StateProviders? stateProviders) => + (super.noSuchMethod( Invocation.method( #loadFromStorage, - [], + [stateProviders], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i4.Future<_i3.Session>.value(_FakeSession_2( + this, + Invocation.method( + #loadFromStorage, + [stateProviders], + ), + )), + returnValueForMissingStub: _i4.Future<_i3.Session>.value(_FakeSession_2( + this, + Invocation.method( + #loadFromStorage, + [stateProviders], + ), + )), + ) as _i4.Future<_i3.Session>); + @override - _i4.Future loadFromRemote( - _i3.Session? session, - _i9.Profile? profile, - ) => + _i4.Future<_i3.Session> loadFromRemote(_i10.StateProviders? stateProviders) => (super.noSuchMethod( Invocation.method( #loadFromRemote, - [ - session, - profile, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - void restoreSession( - String? username, - String? password, - List? faculties, - ) => - super.noSuchMethod( - Invocation.method( - #restoreSession, - [ - username, - password, - faculties, - ], + [stateProviders], ), - returnValueForMissingStub: null, - ); + returnValue: _i4.Future<_i3.Session>.value(_FakeSession_2( + this, + Invocation.method( + #loadFromRemote, + [stateProviders], + ), + )), + returnValueForMissingStub: _i4.Future<_i3.Session>.value(_FakeSession_2( + this, + Invocation.method( + #loadFromRemote, + [stateProviders], + ), + )), + ) as _i4.Future<_i3.Session>); + @override _i4.Future postAuthentication( - _i10.BuildContext? context, + _i11.BuildContext? context, String? username, - String? password, - List? faculties, { + String? password, { required bool? persistentSession, }) => (super.noSuchMethod( @@ -506,31 +539,33 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { context, username, password, - faculties, ], {#persistentSession: persistentSession}, ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override - void markAsNotInitialized() => super.noSuchMethod( + void setState(_i3.Session? newState) => super.noSuchMethod( Invocation.method( - #markAsNotInitialized, - [], + #setState, + [newState], ), returnValueForMissingStub: null, ); + @override - void updateStatus(_i8.RequestStatus? status) => super.noSuchMethod( + void invalidate() => super.noSuchMethod( Invocation.method( - #updateStatus, - [status], + #invalidate, + [], ), returnValueForMissingStub: null, ); + @override - _i4.Future forceRefresh(_i10.BuildContext? context) => + _i4.Future forceRefresh(_i11.BuildContext? context) => (super.noSuchMethod( Invocation.method( #forceRefresh, @@ -539,8 +574,9 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override - _i4.Future ensureInitialized(_i10.BuildContext? context) => + _i4.Future ensureInitialized(_i11.BuildContext? context) => (super.noSuchMethod( Invocation.method( #ensureInitialized, @@ -549,41 +585,25 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override - _i4.Future ensureInitializedFromRemote(_i10.BuildContext? context) => - (super.noSuchMethod( - Invocation.method( - #ensureInitializedFromRemote, - [context], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future ensureInitializedFromStorage() => (super.noSuchMethod( - Invocation.method( - #ensureInitializedFromStorage, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], ), returnValueForMissingStub: null, ); + @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], ), returnValueForMissingStub: null, ); + @override void dispose() => super.noSuchMethod( Invocation.method( @@ -592,6 +612,7 @@ class MockSessionProvider extends _i1.Mock implements _i7.SessionProvider { ), returnValueForMissingStub: null, ); + @override void notifyListeners() => super.noSuchMethod( Invocation.method( diff --git a/uni/test/mocks/unit/providers/exams_provider_test.mocks.dart b/uni/test/mocks/unit/providers/exams_provider_test.mocks.dart index fd78853db..df42b853e 100644 --- a/uni/test/mocks/unit/providers/exams_provider_test.mocks.dart +++ b/uni/test/mocks/unit/providers/exams_provider_test.mocks.dart @@ -1,17 +1,18 @@ -// Mocks generated by Mockito 5.4.3-wip from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in uni/test/unit/providers/exams_provider_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'dart:convert' as _i4; -import 'dart:typed_data' as _i5; +import 'dart:typed_data' as _i6; import 'package:http/http.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:uni/controller/parsers/parser_exams.dart' as _i6; -import 'package:uni/model/entities/course.dart' as _i8; -import 'package:uni/model/entities/exam.dart' as _i7; +import 'package:mockito/src/dummies.dart' as _i5; +import 'package:uni/controller/parsers/parser_exams.dart' as _i7; +import 'package:uni/model/entities/course.dart' as _i9; +import 'package:uni/model/entities/exam.dart' as _i8; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -80,6 +81,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> get( Uri? url, { @@ -109,6 +111,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> post( Uri? url, { @@ -152,6 +155,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> put( Uri? url, { @@ -195,6 +199,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> patch( Uri? url, { @@ -238,6 +243,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future<_i2.Response> delete( Uri? url, { @@ -281,6 +287,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.Response>); + @override _i3.Future read( Uri? url, { @@ -292,11 +299,27 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i3.Future.value(''), - returnValueForMissingStub: _i3.Future.value(''), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), ) as _i3.Future); + @override - _i3.Future<_i5.Uint8List> readBytes( + _i3.Future<_i6.Uint8List> readBytes( Uri? url, { Map? headers, }) => @@ -306,10 +329,11 @@ class MockClient extends _i1.Mock implements _i2.Client { [url], {#headers: headers}, ), - returnValue: _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), returnValueForMissingStub: - _i3.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - ) as _i3.Future<_i5.Uint8List>); + _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + @override _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( @@ -334,6 +358,7 @@ class MockClient extends _i1.Mock implements _i2.Client { ), )), ) as _i3.Future<_i2.StreamedResponse>); + @override void close() => super.noSuchMethod( Invocation.method( @@ -347,20 +372,33 @@ class MockClient extends _i1.Mock implements _i2.Client { /// A class which mocks [ParserExams]. /// /// See the documentation for Mockito's code generation for more information. -class MockParserExams extends _i1.Mock implements _i6.ParserExams { +class MockParserExams extends _i1.Mock implements _i7.ParserExams { @override String getExamSeasonAbbr(String? seasonStr) => (super.noSuchMethod( Invocation.method( #getExamSeasonAbbr, [seasonStr], ), - returnValue: '', - returnValueForMissingStub: '', + returnValue: _i5.dummyValue( + this, + Invocation.method( + #getExamSeasonAbbr, + [seasonStr], + ), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.method( + #getExamSeasonAbbr, + [seasonStr], + ), + ), ) as String); + @override - _i3.Future> parseExams( + _i3.Future> parseExams( _i2.Response? response, - _i8.Course? course, + _i9.Course? course, ) => (super.noSuchMethod( Invocation.method( @@ -370,10 +408,10 @@ class MockParserExams extends _i1.Mock implements _i6.ParserExams { course, ], ), - returnValue: _i3.Future>.value(<_i7.Exam>{}), + returnValue: _i3.Future>.value(<_i8.Exam>{}), returnValueForMissingStub: - _i3.Future>.value(<_i7.Exam>{}), - ) as _i3.Future>); + _i3.Future>.value(<_i8.Exam>{}), + ) as _i3.Future>); } /// A class which mocks [Response]. @@ -381,35 +419,46 @@ class MockParserExams extends _i1.Mock implements _i6.ParserExams { /// See the documentation for Mockito's code generation for more information. class MockResponse extends _i1.Mock implements _i2.Response { @override - _i5.Uint8List get bodyBytes => (super.noSuchMethod( + _i6.Uint8List get bodyBytes => (super.noSuchMethod( Invocation.getter(#bodyBytes), - returnValue: _i5.Uint8List(0), - returnValueForMissingStub: _i5.Uint8List(0), - ) as _i5.Uint8List); + returnValue: _i6.Uint8List(0), + returnValueForMissingStub: _i6.Uint8List(0), + ) as _i6.Uint8List); + @override String get body => (super.noSuchMethod( Invocation.getter(#body), - returnValue: '', - returnValueForMissingStub: '', + returnValue: _i5.dummyValue( + this, + Invocation.getter(#body), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.getter(#body), + ), ) as String); + @override int get statusCode => (super.noSuchMethod( Invocation.getter(#statusCode), returnValue: 0, returnValueForMissingStub: 0, ) as int); + @override Map get headers => (super.noSuchMethod( Invocation.getter(#headers), returnValue: {}, returnValueForMissingStub: {}, ) as Map); + @override bool get isRedirect => (super.noSuchMethod( Invocation.getter(#isRedirect), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override bool get persistentConnection => (super.noSuchMethod( Invocation.getter(#persistentConnection), diff --git a/uni/test/mocks/unit/providers/lecture_provider_test.mocks.dart b/uni/test/mocks/unit/providers/lecture_provider_test.mocks.dart index 3fd2208a6..80287bc39 100644 --- a/uni/test/mocks/unit/providers/lecture_provider_test.mocks.dart +++ b/uni/test/mocks/unit/providers/lecture_provider_test.mocks.dart @@ -1,19 +1,21 @@ -// Mocks generated by Mockito 5.4.3-wip from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in uni/test/unit/providers/lecture_provider_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; -import 'dart:convert' as _i8; -import 'dart:typed_data' as _i9; +import 'dart:convert' as _i9; +import 'dart:typed_data' as _i11; -import 'package:http/http.dart' as _i3; +import 'package:http/http.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i10; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart' - as _i2; + as _i3; import 'package:uni/model/entities/lecture.dart' as _i5; import 'package:uni/model/entities/profile.dart' as _i7; import 'package:uni/model/entities/session.dart' as _i6; +import 'package:uni/model/utils/time/week.dart' as _i8; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,8 +30,8 @@ import 'package:uni/model/entities/session.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDates_0 extends _i1.SmartFake implements _i2.Dates { - _FakeDates_0( +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( Object parent, Invocation parentInvocation, ) : super( @@ -38,19 +40,9 @@ class _FakeDates_0 extends _i1.SmartFake implements _i2.Dates { ); } -class _FakeResponse_1 extends _i1.SmartFake implements _i3.Response { - _FakeResponse_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeStreamedResponse_2 extends _i1.SmartFake - implements _i3.StreamedResponse { - _FakeStreamedResponse_2( +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( Object parent, Invocation parentInvocation, ) : super( @@ -62,7 +54,7 @@ class _FakeStreamedResponse_2 extends _i1.SmartFake /// A class which mocks [ScheduleFetcher]. /// /// See the documentation for Mockito's code generation for more information. -class MockScheduleFetcher extends _i1.Mock implements _i2.ScheduleFetcher { +class MockScheduleFetcher extends _i1.Mock implements _i3.ScheduleFetcher { @override _i4.Future> getLectures( _i6.Session? session, @@ -80,27 +72,27 @@ class MockScheduleFetcher extends _i1.Mock implements _i2.ScheduleFetcher { returnValueForMissingStub: _i4.Future>.value(<_i5.Lecture>[]), ) as _i4.Future>); + @override - _i2.Dates getDates() => (super.noSuchMethod( + List<_i8.Week> getWeeks(DateTime? now) => (super.noSuchMethod( + Invocation.method( + #getWeeks, + [now], + ), + returnValue: <_i8.Week>[], + returnValueForMissingStub: <_i8.Week>[], + ) as List<_i8.Week>); + + @override + List<_i3.Dates> getDates() => (super.noSuchMethod( Invocation.method( #getDates, [], ), - returnValue: _FakeDates_0( - this, - Invocation.method( - #getDates, - [], - ), - ), - returnValueForMissingStub: _FakeDates_0( - this, - Invocation.method( - #getDates, - [], - ), - ), - ) as _i2.Dates); + returnValue: <_i3.Dates>[], + returnValueForMissingStub: <_i3.Dates>[], + ) as List<_i3.Dates>); + @override List getEndpoints(_i6.Session? session) => (super.noSuchMethod( Invocation.method( @@ -115,9 +107,9 @@ class MockScheduleFetcher extends _i1.Mock implements _i2.ScheduleFetcher { /// A class which mocks [Client]. /// /// See the documentation for Mockito's code generation for more information. -class MockClient extends _i1.Mock implements _i3.Client { +class MockClient extends _i1.Mock implements _i2.Client { @override - _i4.Future<_i3.Response> head( + _i4.Future<_i2.Response> head( Uri? url, { Map? headers, }) => @@ -127,7 +119,7 @@ class MockClient extends _i1.Mock implements _i3.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #head, @@ -136,7 +128,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #head, @@ -144,9 +136,10 @@ class MockClient extends _i1.Mock implements _i3.Client { {#headers: headers}, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override - _i4.Future<_i3.Response> get( + _i4.Future<_i2.Response> get( Uri? url, { Map? headers, }) => @@ -156,7 +149,7 @@ class MockClient extends _i1.Mock implements _i3.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #get, @@ -165,7 +158,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #get, @@ -173,13 +166,14 @@ class MockClient extends _i1.Mock implements _i3.Client { {#headers: headers}, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override - _i4.Future<_i3.Response> post( + _i4.Future<_i2.Response> post( Uri? url, { Map? headers, Object? body, - _i8.Encoding? encoding, + _i9.Encoding? encoding, }) => (super.noSuchMethod( Invocation.method( @@ -191,7 +185,7 @@ class MockClient extends _i1.Mock implements _i3.Client { #encoding: encoding, }, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #post, @@ -204,7 +198,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #post, @@ -216,13 +210,14 @@ class MockClient extends _i1.Mock implements _i3.Client { }, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override - _i4.Future<_i3.Response> put( + _i4.Future<_i2.Response> put( Uri? url, { Map? headers, Object? body, - _i8.Encoding? encoding, + _i9.Encoding? encoding, }) => (super.noSuchMethod( Invocation.method( @@ -234,7 +229,7 @@ class MockClient extends _i1.Mock implements _i3.Client { #encoding: encoding, }, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #put, @@ -247,7 +242,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #put, @@ -259,13 +254,14 @@ class MockClient extends _i1.Mock implements _i3.Client { }, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override - _i4.Future<_i3.Response> patch( + _i4.Future<_i2.Response> patch( Uri? url, { Map? headers, Object? body, - _i8.Encoding? encoding, + _i9.Encoding? encoding, }) => (super.noSuchMethod( Invocation.method( @@ -277,7 +273,7 @@ class MockClient extends _i1.Mock implements _i3.Client { #encoding: encoding, }, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #patch, @@ -290,7 +286,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #patch, @@ -302,13 +298,14 @@ class MockClient extends _i1.Mock implements _i3.Client { }, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override - _i4.Future<_i3.Response> delete( + _i4.Future<_i2.Response> delete( Uri? url, { Map? headers, Object? body, - _i8.Encoding? encoding, + _i9.Encoding? encoding, }) => (super.noSuchMethod( Invocation.method( @@ -320,7 +317,7 @@ class MockClient extends _i1.Mock implements _i3.Client { #encoding: encoding, }, ), - returnValue: _i4.Future<_i3.Response>.value(_FakeResponse_1( + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #delete, @@ -333,7 +330,7 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.Response>.value(_FakeResponse_1( + _i4.Future<_i2.Response>.value(_FakeResponse_0( this, Invocation.method( #delete, @@ -345,7 +342,8 @@ class MockClient extends _i1.Mock implements _i3.Client { }, ), )), - ) as _i4.Future<_i3.Response>); + ) as _i4.Future<_i2.Response>); + @override _i4.Future read( Uri? url, { @@ -357,11 +355,27 @@ class MockClient extends _i1.Mock implements _i3.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future.value(''), - returnValueForMissingStub: _i4.Future.value(''), + returnValue: _i4.Future.value(_i10.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i4.Future.value(_i10.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), ) as _i4.Future); + @override - _i4.Future<_i9.Uint8List> readBytes( + _i4.Future<_i11.Uint8List> readBytes( Uri? url, { Map? headers, }) => @@ -371,19 +385,20 @@ class MockClient extends _i1.Mock implements _i3.Client { [url], {#headers: headers}, ), - returnValue: _i4.Future<_i9.Uint8List>.value(_i9.Uint8List(0)), + returnValue: _i4.Future<_i11.Uint8List>.value(_i11.Uint8List(0)), returnValueForMissingStub: - _i4.Future<_i9.Uint8List>.value(_i9.Uint8List(0)), - ) as _i4.Future<_i9.Uint8List>); + _i4.Future<_i11.Uint8List>.value(_i11.Uint8List(0)), + ) as _i4.Future<_i11.Uint8List>); + @override - _i4.Future<_i3.StreamedResponse> send(_i3.BaseRequest? request) => + _i4.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( Invocation.method( #send, [request], ), returnValue: - _i4.Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_2( + _i4.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( this, Invocation.method( #send, @@ -391,14 +406,15 @@ class MockClient extends _i1.Mock implements _i3.Client { ), )), returnValueForMissingStub: - _i4.Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_2( + _i4.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( this, Invocation.method( #send, [request], ), )), - ) as _i4.Future<_i3.StreamedResponse>); + ) as _i4.Future<_i2.StreamedResponse>); + @override void close() => super.noSuchMethod( Invocation.method( @@ -412,37 +428,48 @@ class MockClient extends _i1.Mock implements _i3.Client { /// A class which mocks [Response]. /// /// See the documentation for Mockito's code generation for more information. -class MockResponse extends _i1.Mock implements _i3.Response { +class MockResponse extends _i1.Mock implements _i2.Response { @override - _i9.Uint8List get bodyBytes => (super.noSuchMethod( + _i11.Uint8List get bodyBytes => (super.noSuchMethod( Invocation.getter(#bodyBytes), - returnValue: _i9.Uint8List(0), - returnValueForMissingStub: _i9.Uint8List(0), - ) as _i9.Uint8List); + returnValue: _i11.Uint8List(0), + returnValueForMissingStub: _i11.Uint8List(0), + ) as _i11.Uint8List); + @override String get body => (super.noSuchMethod( Invocation.getter(#body), - returnValue: '', - returnValueForMissingStub: '', + returnValue: _i10.dummyValue( + this, + Invocation.getter(#body), + ), + returnValueForMissingStub: _i10.dummyValue( + this, + Invocation.getter(#body), + ), ) as String); + @override int get statusCode => (super.noSuchMethod( Invocation.getter(#statusCode), returnValue: 0, returnValueForMissingStub: 0, ) as int); + @override Map get headers => (super.noSuchMethod( Invocation.getter(#headers), returnValue: {}, returnValueForMissingStub: {}, ) as Map); + @override bool get isRedirect => (super.noSuchMethod( Invocation.getter(#isRedirect), returnValue: false, returnValueForMissingStub: false, ) as bool); + @override bool get persistentConnection => (super.noSuchMethod( Invocation.getter(#persistentConnection), diff --git a/uni/test/test_widget.dart b/uni/test/test_widget.dart index 07dcd7d47..8021138fb 100644 --- a/uni/test/test_widget.dart +++ b/uni/test/test_widget.dart @@ -1,10 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/app_locale.dart'; +import 'package:uni/model/entities/profile.dart'; +import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/view/locale_notifier.dart'; +Future initTestEnvironment() async { + SharedPreferences.setMockInitialValues({}); + PreferencesController.prefs = await SharedPreferences.getInstance(); + databaseFactory = databaseFactoryFfi; +} + Widget testableWidget( Widget widget, { List providers = const [], @@ -14,13 +25,16 @@ Widget testableWidget( ChangeNotifierProvider( create: (_) => LocaleNotifier(AppLocale.pt), ), + ChangeNotifierProvider( + create: (_) => ProfileProvider()..setState(Profile()), + ), ...providers, ], - child: wrapWidget(widget), + child: _wrapWidget(widget), ); } -Widget wrapWidget(Widget widget) { +Widget _wrapWidget(Widget widget) { return MaterialApp( localizationsDelegates: const [ S.delegate, diff --git a/uni/test/unit/models/utils/time/week_test.dart b/uni/test/unit/models/utils/time/week_test.dart new file mode 100644 index 000000000..ba4f86e59 --- /dev/null +++ b/uni/test/unit/models/utils/time/week_test.dart @@ -0,0 +1,194 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:uni/model/utils/time/week.dart'; + +void main() { + group('Week', () { + final week1 = Week(start: DateTime(2024, 2, 29)); + final week2 = Week(start: DateTime(2024, 3, 3)); + + group('startingOn', () { + test('should return the same week if the starting day is the same', () { + expect(week1.startingOn(DateTime.thursday), week1); + expect(week2.startingOn(DateTime.sunday), week2); + }); + + group( + 'should return a week that starts in the given weekday, after the' + " current week's start and before the next one's", () { + test('week1, starting on Sunday', () { + expect( + week1.startingOn(DateTime.sunday).start.weekday, + DateTime.sunday, + ); + + expect( + week1.startingOn(DateTime.sunday).start.isAfter(week1.start), + true, + ); + + expect( + week1 + .startingOn(DateTime.sunday) + .start + .isBefore(week1.next().start), + true, + ); + }); + + test('week1, ending on Monday', () { + expect( + week1.startingOn(DateTime.monday).start.weekday, + DateTime.monday, + ); + + expect( + week1.startingOn(DateTime.monday).start.isAfter(week1.start), + true, + ); + + expect( + week1 + .startingOn(DateTime.monday) + .start + .isBefore(week1.next().start), + true, + ); + }); + + test('week2, ending on Monday', () { + expect( + week2.startingOn(DateTime.monday).start.weekday, + DateTime.monday, + ); + + expect( + week2.startingOn(DateTime.monday).start.isAfter(week2.start), + true, + ); + + expect( + week2 + .startingOn(DateTime.monday) + .start + .isBefore(week2.next().start), + true, + ); + }); + + test('week2, ending on Saturday', () { + expect( + week2.startingOn(DateTime.saturday).start.weekday, + DateTime.saturday, + ); + + expect( + week2.startingOn(DateTime.saturday).start.isAfter(week2.start), + true, + ); + + expect( + week2 + .startingOn(DateTime.saturday) + .start + .isBefore(week2.next().start), + true, + ); + }); + }); + }); + + group('endingOn', () { + test('should return the same week if the ending day is the same', () { + expect(week1.endingOn(DateTime.thursday), week1); + expect(week2.endingOn(DateTime.sunday), week2); + }); + + group( + 'should return a week that ends in the given weekday, before the' + " current week's end and after the previous one's", () { + test('week1, ending on Sunday', () { + expect( + week1.endingOn(DateTime.sunday).end.weekday, + DateTime.sunday, + ); + + expect( + week1.endingOn(DateTime.sunday).end.isBefore(week1.end), + true, + ); + + expect( + week1.endingOn(DateTime.sunday).end.isAfter(week1.previous().end), + true, + ); + }); + + test('week1, ending on Monday', () { + expect( + week1.endingOn(DateTime.monday).end.weekday, + DateTime.monday, + ); + + expect( + week1.endingOn(DateTime.monday).end.isBefore(week1.end), + true, + ); + + expect( + week1.endingOn(DateTime.monday).end.isAfter(week1.previous().end), + true, + ); + }); + + test('week2, ending on Monday', () { + expect( + week2.endingOn(DateTime.monday).end.weekday, + DateTime.monday, + ); + + expect( + week2.endingOn(DateTime.monday).end.isBefore(week2.end), + true, + ); + + expect( + week2.endingOn(DateTime.monday).end.isAfter(week1.previous().end), + true, + ); + }); + + test('week2, ending on Saturday', () { + expect( + week2.endingOn(DateTime.saturday).end.weekday, + DateTime.saturday, + ); + + expect( + week2.endingOn(DateTime.saturday).end.isBefore(week2.end), + true, + ); + + expect( + week2.endingOn(DateTime.saturday).end.isAfter(week1.previous().end), + true, + ); + }); + }); + }); + + group('getWeekday', () { + group( + 'should return a day within the same week, with the' + ' requested weekday', () { + for (var i = 0; i < 7; i++) { + test('[DateTime.weekday = ${i + 1}]', () { + final weekday = DateTime.monday + i; + final day = week1.getWeekday(weekday); + expect(day.weekday, weekday); + expect(week1.contains(day), true); + }); + } + }); + }); + }); +} diff --git a/uni/test/unit/models/utils/time/weekday_mapper_test.dart b/uni/test/unit/models/utils/time/weekday_mapper_test.dart new file mode 100644 index 000000000..ab4eb52c8 --- /dev/null +++ b/uni/test/unit/models/utils/time/weekday_mapper_test.dart @@ -0,0 +1,176 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:uni/model/utils/time/weekday_mapper.dart'; + +void exhaustivelyTestWeekdayMapper( + int fromStart, + int fromMonday, + int toStart, + int toMonday, +) { + group( + 'When mapping from $fromStart..${fromStart + 6} (monday = $fromMonday)' + ' to $toStart..${toStart + 6} (monday = $toMonday)', () { + final mapper = WeekdayMapper( + fromStart: fromStart, + fromMonday: fromMonday, + toStart: toStart, + toMonday: toMonday, + ); + + final inverseMapper = mapper.inverse; + + final fromEnd = fromStart + 7; + final toEnd = toStart + 7; + + var fromWeekday = fromMonday; + var toWeekday = toMonday; + + for (var i = 0; i < 7; i++) { + test( + '[DateTime.weekday = ${i + 1}] fromWeekday = $fromWeekday should' + ' map to toWeekday = $toMonday', () { + expect(mapper.map(fromWeekday), toWeekday); + }); + + test( + '[DateTime.weekday = ${i + 1}] toWeekday = $toWeekday should' + ' inversely map to fromWeekday = $fromWeekday', () { + expect(inverseMapper.map(toWeekday), fromWeekday); + }); + + fromWeekday = ++fromWeekday >= fromEnd ? fromStart : fromWeekday; + toWeekday = ++toWeekday >= toEnd ? toStart : toWeekday; + } + }); +} + +void ensureMapperEquivalenceByStartWeekdays( + int fromStart, + int fromStartWeekday, + int toStart, + int toStartWeekday, +) { + group( + 'When mapping from $fromStart..${fromStart + 6}' + ' (start.weekday = $fromStartWeekday to $toStart..${toStart + 6}' + ' (start.weekday = $toStartWeekday)', () { + test('should be correctly created', () { + final equivalent = WeekdayMapper( + fromStart: fromStart, + fromMonday: WeekdayMapper.fromStartWeekdays( + fromStart: 1, + fromStartWeekday: DateTime.monday, + toStart: fromStart, + toStartWeekday: fromStartWeekday, + ).map(DateTime.monday), + toStart: toStart, + toMonday: WeekdayMapper.fromStartWeekdays( + fromStart: 1, + fromStartWeekday: DateTime.monday, + toStart: toStart, + toStartWeekday: toStartWeekday, + ).map(DateTime.monday), + ); + + final mapper = WeekdayMapper.fromStartWeekdays( + fromStart: fromStart, + fromStartWeekday: fromStartWeekday, + toStart: toStart, + toStartWeekday: toStartWeekday, + ); + + expect(mapper, equivalent); + }); + }); +} + +void main() { + group('WeekdayMapper', () { + group('map', () { + // There are three main states for `fromStart` and `toStart`: + // - fromStart < toStart + // - fromStart = toStart + // - fromStart > toStart + + // The same goes for `fromMonday` and `toMonday`. + + exhaustivelyTestWeekdayMapper(2, 4, 3, 5); + exhaustivelyTestWeekdayMapper(2, 4, 3, 4); + exhaustivelyTestWeekdayMapper(2, 5, 3, 4); + + exhaustivelyTestWeekdayMapper(2, 4, 2, 5); + exhaustivelyTestWeekdayMapper(2, 4, 2, 4); + exhaustivelyTestWeekdayMapper(2, 5, 2, 4); + + exhaustivelyTestWeekdayMapper(3, 4, 2, 5); + exhaustivelyTestWeekdayMapper(3, 4, 2, 4); + exhaustivelyTestWeekdayMapper(3, 5, 2, 4); + }); + }); + + group('constructor fromStartWeekdays', () { + // There are three main states for `fromStart` and `toStart`: + // - fromStart < toStart + // - fromStart = toStart + // - fromStart > toStart + + // The same goes for `fromStartWeekday` and `toStartWeekday`. + + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.tuesday, + 3, + DateTime.thursday, + ); + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.wednesday, + 3, + DateTime.wednesday, + ); + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.thursday, + 3, + DateTime.tuesday, + ); + + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.tuesday, + 2, + DateTime.thursday, + ); + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.wednesday, + 2, + DateTime.wednesday, + ); + ensureMapperEquivalenceByStartWeekdays( + 2, + DateTime.thursday, + 2, + DateTime.tuesday, + ); + + ensureMapperEquivalenceByStartWeekdays( + 3, + DateTime.tuesday, + 2, + DateTime.thursday, + ); + ensureMapperEquivalenceByStartWeekdays( + 3, + DateTime.wednesday, + 2, + DateTime.wednesday, + ); + ensureMapperEquivalenceByStartWeekdays( + 3, + DateTime.thursday, + 2, + DateTime.tuesday, + ); + }); +} diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index d424a2a48..ec59490d9 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -13,11 +13,14 @@ import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/model/request_status.dart'; import '../../mocks/unit/providers/exams_provider_test.mocks.dart'; +import '../../test_widget.dart'; @GenerateNiceMocks( [MockSpec(), MockSpec(), MockSpec()], ) -void main() { +void main() async { + await initTestEnvironment(); + group('ExamProvider', () { final mockClient = MockClient(); final parserExams = MockParserExams(); @@ -73,14 +76,14 @@ void main() { setUp(() { provider = ExamProvider(); - expect(provider.status, RequestStatus.busy); + expect(provider.requestStatus, RequestStatus.busy); }); test('When given one exam', () async { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -88,15 +91,17 @@ void main() { persistentSession: false, ); - expect(provider.exams.isNotEmpty, true); - expect(provider.exams, [sopeExam]); + provider.setState(exams); + + expect(provider.state!.isNotEmpty, true); + expect(provider.state, [sopeExam]); }); test('When given two exams', () async { when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -104,7 +109,9 @@ void main() { persistentSession: false, ); - expect(provider.exams, [sopeExam, sdisExam]); + provider.setState(exams); + + expect(provider.state, [sopeExam, sdisExam]); }); test(''' @@ -125,7 +132,7 @@ When given three exams but one is to be parsed out, when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam, specialExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -133,7 +140,9 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.exams, [sopeExam, sdisExam]); + provider.setState(exams); + + expect(provider.state, [sopeExam, sdisExam]); }); test('When an error occurs while trying to obtain the exams', () async { @@ -167,7 +176,7 @@ When given three exams but one is to be parsed out, when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -175,7 +184,9 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.exams, [todayExam]); + provider.setState(exams); + + expect(provider.state, [todayExam]); }); test('When Exam was one hour ago', () async { @@ -194,7 +205,7 @@ When given three exams but one is to be parsed out, when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -202,7 +213,9 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.exams, []); + provider.setState(exams); + + expect(provider.state, []); }); test('When Exam is ocurring', () async { @@ -221,7 +234,7 @@ When given three exams but one is to be parsed out, when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); - await provider.fetchUserExams( + final exams = await provider.fetchUserExams( parserExams, profile, session, @@ -229,7 +242,9 @@ When given three exams but one is to be parsed out, persistentSession: false, ); - expect(provider.exams, [todayExam]); + provider.setState(exams); + + expect(provider.state, [todayExam]); }); }); } diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index 78a1bbb3b..e9c870c4c 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -12,11 +12,14 @@ import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/request_status.dart'; import '../../mocks/unit/providers/lecture_provider_test.mocks.dart'; +import '../../test_widget.dart'; @GenerateNiceMocks( [MockSpec(), MockSpec(), MockSpec()], ) -void main() { +void main() async { + await initTestEnvironment(); + group('Schedule Action Creator', () { final fetcherMock = MockScheduleFetcher(); final mockClient = MockClient(); @@ -56,21 +59,23 @@ void main() { late LectureProvider provider; setUp(() { provider = LectureProvider(); - expect(provider.status, RequestStatus.busy); + expect(provider.requestStatus, RequestStatus.busy); }); test('When given a single schedule', () async { when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => [lecture1, lecture2]); - await provider.fetchUserLectures( + final lectures = await provider.fetchUserLectures( session, profile, fetcher: fetcherMock, persistentSession: false, ); - expect(provider.lectures, [lecture1, lecture2]); + provider.setState(lectures); + + expect(provider.state, [lecture1, lecture2]); }); test('When an error occurs while trying to obtain the schedule', () async { 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 d4e3a227f..b8d52f718 100644 --- a/uni/test/unit/view/Pages/exams_page_view_test.dart +++ b/uni/test/unit/view/Pages/exams_page_view_test.dart @@ -10,7 +10,9 @@ import '../../../test_widget.dart'; class MockExamProvider extends Mock implements ExamProvider {} -void main() { +void main() async { + await initTestEnvironment(); + group('ExamsPage', () { const firstExamSubject = 'SOPE'; const firstExamDate = '2019-09-11'; @@ -19,7 +21,7 @@ void main() { testWidgets('When given an empty list', (WidgetTester tester) async { const widget = ExamsPageView(); - final examProvider = ExamProvider()..exams = []; + final examProvider = ExamProvider()..setState([]); final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; @@ -43,12 +45,12 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider()..exams = [firstExam]; + final examProvider = ExamProvider()..setState([firstExam]); final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.byKey(Key(firstExam.toString())), findsOneWidget); expect(find.byKey(Key('$firstExam-exam')), findsOneWidget); @@ -86,12 +88,12 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider()..exams = examList; + final examProvider = ExamProvider()..setState(examList); final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); - await tester.pump(); + await tester.pumpAndSettle(); expect( find.byKey(Key(examList.map((ex) => ex.toString()).join())), @@ -132,12 +134,12 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider()..exams = examList; + final examProvider = ExamProvider()..setState(examList); final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.byKey(Key(firstExam.toString())), findsOneWidget); expect(find.byKey(Key(secondExam.toString())), findsOneWidget); @@ -196,7 +198,7 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider()..exams = examList; + final examProvider = ExamProvider()..setState(examList); final firstDayKey = [firstExam, secondExam].map((ex) => ex.toString()).join(); @@ -206,7 +208,7 @@ void main() { final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.byKey(Key(firstDayKey)), findsOneWidget); expect(find.byKey(Key(secondDayKey)), 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 59c77e725..0ae252ed7 100644 --- a/uni/test/unit/view/Pages/schedule_page_view_test.dart +++ b/uni/test/unit/view/Pages/schedule_page_view_test.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:uni/model/entities/lecture.dart'; -import 'package:uni/model/request_status.dart'; import 'package:uni/view/schedule/schedule.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import '../../../test_widget.dart'; -void main() { +void main() async { + await initTestEnvironment(); + group('SchedulePage', () { const blocks = 4; const classNumber = 'MIEIC03'; + final now = DateTime(2021, 06, 05); final day0 = DateTime(2021, 06, 07); final day1 = DateTime(2021, 06, 08); final day2 = DateTime(2021, 06, 09); @@ -95,8 +97,8 @@ void main() { testWidgets('When given one lecture on a single day', (WidgetTester tester) async { final widget = SchedulePageView( - lectures: [lecture1], - scheduleStatus: RequestStatus.successful, + [lecture1], + now: now, ); await tester.pumpWidget(testableWidget(widget, providers: [])); @@ -118,8 +120,8 @@ void main() { testWidgets('When given two lectures on a single day', (WidgetTester tester) async { final widget = SchedulePageView( - lectures: [lecture1, lecture2], - scheduleStatus: RequestStatus.successful, + [lecture1, lecture2], + now: now, ); await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); @@ -136,12 +138,13 @@ void main() { findsNWidgets(2), ); }); + testWidgets('When given lectures on different days', (WidgetTester tester) async { final widget = DefaultTabController( length: daysOfTheWeek.length, child: SchedulePageView( - lectures: [ + [ lecture1, lecture2, lecture3, @@ -149,7 +152,7 @@ void main() { lecture5, lecture6, ], - scheduleStatus: RequestStatus.successful, + now: now, ), ); diff --git a/uni/test/unit/view/Widgets/exam_row_test.dart b/uni/test/unit/view/Widgets/exam_row_test.dart index dcdce9505..77bd702c2 100644 --- a/uni/test/unit/view/Widgets/exam_row_test.dart +++ b/uni/test/unit/view/Widgets/exam_row_test.dart @@ -8,7 +8,9 @@ import 'package:uni/view/exams/widgets/exam_row.dart'; import '../../../test_widget.dart'; -void main() { +void main() async { + await initTestEnvironment(); + group('Exam Row', () { const subject = 'SOPE'; final begin = DateTime( @@ -29,7 +31,12 @@ void main() { testWidgets('When given a single room', (WidgetTester tester) async { final rooms = ['B315']; final exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); - final widget = ExamRow(exam: exam, teacher: '', mainPage: true); + final widget = ExamRow( + exam: exam, + teacher: '', + mainPage: true, + onChangeVisibility: () {}, + ); final providers = [ ChangeNotifierProvider(create: (_) => ExamProvider()), @@ -51,7 +58,12 @@ void main() { testWidgets('When multiple rooms', (WidgetTester tester) async { final rooms = ['B315', 'B316', 'B330']; final exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); - final widget = ExamRow(exam: exam, teacher: '', mainPage: true); + final widget = ExamRow( + exam: exam, + teacher: '', + mainPage: true, + onChangeVisibility: () {}, + ); final providers = [ ChangeNotifierProvider(create: (_) => ExamProvider()), diff --git a/uni/test/unit/view/Widgets/schedule_slot_test.dart b/uni/test/unit/view/Widgets/schedule_slot_test.dart index 4b06fdacc..702d4007f 100644 --- a/uni/test/unit/view/Widgets/schedule_slot_test.dart +++ b/uni/test/unit/view/Widgets/schedule_slot_test.dart @@ -5,7 +5,9 @@ import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import '../../../test_widget.dart'; -void main() { +void main() async { + await initTestEnvironment(); + group('Schedule Slot', () { const subject = 'SOPE'; final begin = DateTime(2021, 06, 01, 10); diff --git a/uni/windows/flutter/generated_plugins.cmake b/uni/windows/flutter/generated_plugins.cmake index a7e9181a9..353256099 100644 --- a/uni/windows/flutter/generated_plugins.cmake +++ b/uni/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + battery_plus connectivity_plus sentry_flutter url_launcher_windows