diff --git a/README.md b/README.md index 13787ab54..68355e652 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ [![Build badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/uni/format_lint_test.yaml?style=for-the-badge)](https://github.com/NIAEFEUP/uni/actions) [![Deploy badge](https://img.shields.io/github/actions/workflow/status/NIAEFEUP/uni/deploy.yaml?label=Deploy&style=for-the-badge)](https://github.com/NIAEFEUP/uni/actions) + +[![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg?style=for-the-badge)](https://pub.dev/packages/very_good_analysis) [![License badge](https://img.shields.io/github/license/NIAEFEUP/uni?style=for-the-badge)](https://github.com/NIAEFEUP/uni/blob/develop/LICENSE) Get it on Google Play diff --git a/uni/analysis_options.yaml b/uni/analysis_options.yaml index adeb51dc6..be33d4712 100644 --- a/uni/analysis_options.yaml +++ b/uni/analysis_options.yaml @@ -1,38 +1,16 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +include: package:very_good_analysis/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + # Exclude auto-generated files from dart analysis + exclude: + - '**.g.dart' + - '**.freezed.dart' +# Custom linter rules. A list of all rules can be found at +# https://dart-lang.github.io/linter/lints/options/options.html linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - always_use_package_imports: true - prefer_final_locals: true - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + public_member_api_docs: false + avoid_equals_and_hash_code_on_mutable_classes: false + one_member_abstracts: false -analyzer: - errors: - # allow for asset_does_not_exist - unrecognized_error_code: ignore - # ignore this in pubspec.yaml - asset_directory_does_not_exist: ignore \ No newline at end of file diff --git a/uni/lib/controller/background_workers/background_callback.dart b/uni/lib/controller/background_workers/background_callback.dart index 2c0833c0f..9c9f45536 100644 --- a/uni/lib/controller/background_workers/background_callback.dart +++ b/uni/lib/controller/background_workers/background_callback.dart @@ -7,24 +7,26 @@ import 'package:workmanager/workmanager.dart'; /// the bool is all functions that are ran by backgroundfetch in iOS /// (they must not take any arguments, not checked) const taskMap = { - "pt.up.fe.ni.uni.notificationworker": + 'pt.up.fe.ni.uni.notificationworker': Tuple2(NotificationManager.updateAndTriggerNotifications, true) }; @pragma('vm:entry-point') -// This function is android only and only executes when the app is complety terminated -void workerStartCallback() async { +// This function is android only and only executes +// when the app is completely terminated +Future workerStartCallback() async { Workmanager().executeTask((taskName, inputData) async { try { - Logger().d("""[$taskName]: Start executing job..."""); + Logger().d('''[$taskName]: Start executing job...'''); - //iOSBackgroundTask is a special task, that iOS runs whenever it deems necessary - //and will run all tasks with the flag true - //NOTE: keep the total execution time under 30s to avoid being punished by the iOS scheduler. + // iOSBackgroundTask is a special task, that iOS runs whenever + // it deems necessary and will run all tasks with the flag true + // NOTE: keep the total execution time under 30s to avoid being punished + // by the iOS scheduler. if (taskName == Workmanager.iOSBackgroundTask) { taskMap.forEach((key, value) async { if (value.item2) { - Logger().d("""[$key]: Start executing job..."""); + Logger().d('''[$key]: Start executing job...'''); await value.item1(); } }); @@ -34,7 +36,7 @@ void workerStartCallback() async { //to not be punished by the scheduler in future runs. await taskMap[taskName]!.item1(); } catch (err, stackstrace) { - Logger().e("Error while running $taskName job:", err, stackstrace); + Logger().e('Error while running $taskName job:', err, stackstrace); return false; } return true; diff --git a/uni/lib/controller/background_workers/notifications.dart b/uni/lib/controller/background_workers/notifications.dart index d614116db..f6c7f52f8 100644 --- a/uni/lib/controller/background_workers/notifications.dart +++ b/uni/lib/controller/background_workers/notifications.dart @@ -14,37 +14,49 @@ import 'package:uni/model/entities/session.dart'; import 'package:workmanager/workmanager.dart'; /// -/// Stores all notifications that will be checked and displayed in the background. +/// Stores notifications that will be checked and displayed in the background. /// Add your custom notification here, because it will NOT be added at runtime. /// (due to background worker limitations). /// Map notificationMap = { - TuitionNotification: () => TuitionNotification(), + TuitionNotification: TuitionNotification.new, }; abstract class Notification { + Notification(this.uniqueID, this.timeout); + String uniqueID; Duration timeout; - Notification(this.uniqueID, this.timeout); - Future> buildNotificationContent(Session session); Future shouldDisplay(Session session); - void displayNotification(Tuple2 content, - FlutterLocalNotificationsPlugin localNotificationsPlugin); + void displayNotification( + Tuple2 content, + FlutterLocalNotificationsPlugin localNotificationsPlugin, + ); - Future displayNotificationIfPossible(Session session, - FlutterLocalNotificationsPlugin localNotificationsPlugin) async { + Future displayNotificationIfPossible( + Session session, + FlutterLocalNotificationsPlugin localNotificationsPlugin, + ) async { if (await shouldDisplay(session)) { displayNotification( - await buildNotificationContent(session), localNotificationsPlugin); + await buildNotificationContent(session), + localNotificationsPlugin, + ); } } } class NotificationManager { + factory NotificationManager() { + return _notificationManager; + } + + NotificationManager._internal(); + static final NotificationManager _notificationManager = NotificationManager._internal(); @@ -55,71 +67,77 @@ class NotificationManager { static const Duration _notificationWorkerPeriod = Duration(hours: 1); - factory NotificationManager() { - return _notificationManager; - } - static Future updateAndTriggerNotifications() async { final userInfo = await AppSharedPreferences.getPersistentUserInfo(); final faculties = await AppSharedPreferences.getUserFaculties(); - final Session? session = await NetworkRouter.login( - userInfo.item1, userInfo.item2, faculties, false); + final session = await NetworkRouter.login( + userInfo.item1, + userInfo.item2, + faculties, + persistentSession: false, + ); if (session == null) { return; } - // Get the .json file that contains the last time that the notification has ran - _initFlutterNotificationsPlugin(); + // Get the .json file that contains the last time that the + // notification has ran + await _initFlutterNotificationsPlugin(); final notificationStorage = await NotificationTimeoutStorage.create(); - for (Notification Function() value in notificationMap.values) { - final Notification notification = value(); - final DateTime lastRan = notificationStorage + for (final value in notificationMap.values) { + final notification = value(); + final lastRan = notificationStorage .getLastTimeNotificationExecuted(notification.uniqueID); if (lastRan.add(notification.timeout).isBefore(DateTime.now())) { await notification.displayNotificationIfPossible( - session, _localNotificationsPlugin); + session, + _localNotificationsPlugin, + ); await notificationStorage.addLastTimeNotificationExecuted( - notification.uniqueID, DateTime.now()); + notification.uniqueID, + DateTime.now(), + ); } } } - void initializeNotifications() async { - // guarantees that the execution is only done once in the lifetime of the app. + Future initializeNotifications() async { + // guarantees that the execution is only done + // once in the lifetime of the app. if (_initialized) return; _initialized = true; - _initFlutterNotificationsPlugin(); - _buildNotificationWorker(); + await _initFlutterNotificationsPlugin(); + await _buildNotificationWorker(); } - static void _initFlutterNotificationsPlugin() async { - const AndroidInitializationSettings initializationSettingsAndroid = + static Future _initFlutterNotificationsPlugin() async { + const initializationSettingsAndroid = AndroidInitializationSettings('ic_notification'); //request for notifications immediatly on iOS - const DarwinInitializationSettings darwinInitializationSettings = - DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestCriticalPermission: true); - - const InitializationSettings initializationSettings = - InitializationSettings( - android: initializationSettingsAndroid, - iOS: darwinInitializationSettings, - macOS: darwinInitializationSettings); + const darwinInitializationSettings = DarwinInitializationSettings( + requestCriticalPermission: true, + ); + + const initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: darwinInitializationSettings, + macOS: darwinInitializationSettings, + ); await _localNotificationsPlugin.initialize(initializationSettings); - //specific to android 13+, 12 or lower permission is requested when the first notification channel opens + // specific to android 13+, 12 or lower permission is requested when + // the first notification channel opens if (Platform.isAndroid) { - final AndroidFlutterLocalNotificationsPlugin androidPlugin = - _localNotificationsPlugin.resolvePlatformSpecificImplementation()!; + final androidPlugin = + _localNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()!; try { - final bool? permissionGranted = await androidPlugin.requestPermission(); + final permissionGranted = await androidPlugin.requestPermission(); if (permissionGranted != true) { return; } @@ -127,31 +145,33 @@ class NotificationManager { } } - NotificationManager._internal(); - - static void _buildNotificationWorker() async { + static Future _buildNotificationWorker() async { if (Platform.isAndroid) { - Workmanager().cancelByUniqueName( - "pt.up.fe.ni.uni.notificationworker"); //stop task if it's already running - Workmanager().registerPeriodicTask( - "pt.up.fe.ni.uni.notificationworker", - "pt.up.fe.ni.uni.notificationworker", + await Workmanager().cancelByUniqueName( + 'pt.up.fe.ni.uni.notificationworker', + ); //stop task if it's already running + await Workmanager().registerPeriodicTask( + 'pt.up.fe.ni.uni.notificationworker', + 'pt.up.fe.ni.uni.notificationworker', constraints: Constraints(networkType: NetworkType.connected), frequency: _notificationWorkerPeriod, ); } else if (Platform.isIOS || kIsWeb) { - //This is to guarentee that the notification will be run at least the app starts. - //NOTE (luisd): This is not an isolate because we can't register plugins in a isolate, in the current version of flutter - // so we just do it after login - Logger().d("Running notification worker on main isolate..."); + // This is to guarantee that the notification + // will be run at least once the app starts. + // NOTE (luisd): This is not an isolate because we can't register plugins + // in a isolate, in the current version of flutter + // so we just do it after login + Logger().d('Running notification worker on main isolate...'); await updateAndTriggerNotifications(); Timer.periodic(_notificationWorkerPeriod, (timer) { - Logger().d("Running notification worker on periodic timer..."); + Logger().d('Running notification worker on periodic timer...'); updateAndTriggerNotifications(); }); } else { throw PlatformException( - code: "WorkerManager is only supported in iOS and android..."); + code: 'WorkerManager is only supported in iOS and android...', + ); } } } diff --git a/uni/lib/controller/background_workers/notifications/tuition_notification.dart b/uni/lib/controller/background_workers/notifications/tuition_notification.dart index db6e43abd..738b5ce24 100644 --- a/uni/lib/controller/background_workers/notifications/tuition_notification.dart +++ b/uni/lib/controller/background_workers/notifications/tuition_notification.dart @@ -8,45 +8,57 @@ import 'package:uni/model/entities/session.dart'; import 'package:uni/utils/duration_string_formatter.dart'; class TuitionNotification extends Notification { - late DateTime _dueDate; - TuitionNotification() - : super("tuition-notification", const Duration(hours: 12)); + : super('tuition-notification', const Duration(hours: 12)); + late DateTime _dueDate; @override Future> buildNotificationContent( - Session session) async { - //We must add one day because the time limit is actually at 23:59 and not at 00:00 of the same day + Session session, + ) async { + // We must add one day because the time limit is actually at 23:59 and + // not at 00:00 of the same day if (_dueDate.add(const Duration(days: 1)).isBefore(DateTime.now())) { - final Duration duration = DateTime.now().difference(_dueDate); + final duration = DateTime.now().difference(_dueDate); if (duration.inDays == 0) { - return const Tuple2("⚠️ Ainda não pagaste as propinas ⚠️", - "O prazo para pagar as propinas acabou ontem"); + return const Tuple2( + '⚠️ Ainda não pagaste as propinas ⚠️', + 'O prazo para pagar as propinas acabou ontem', + ); } return Tuple2( - "⚠️ Ainda não pagaste as propinas ⚠️", - duration.toFormattedString("Já passou {} desde a data limite", - "Já passaram {} desde a data limite")); + '⚠️ Ainda não pagaste as propinas ⚠️', + duration.toFormattedString( + 'Já passou {} desde a data limite', + 'Já passaram {} desde a data limite', + ), + ); } - final Duration duration = _dueDate.difference(DateTime.now()); + final duration = _dueDate.difference(DateTime.now()); if (duration.inDays == 0) { - return const Tuple2("O prazo limite para as propinas está a acabar", - "Hoje acaba o prazo para pagamento das propinas!"); + return const Tuple2( + 'O prazo limite para as propinas está a acabar', + 'Hoje acaba o prazo para pagamento das propinas!', + ); } return Tuple2( - "O prazo limite para as propinas está a acabar", - duration.toFormattedString( - "Falta {} para a data limite", "Faltam {} para a data limite")); + 'O prazo limite para as propinas está a acabar', + duration.toFormattedString( + 'Falta {} para a data limite', + 'Faltam {} para a data limite', + ), + ); } @override Future shouldDisplay(Session session) async { - final bool notificationsAreDisabled = + final notificationsAreDisabled = !(await AppSharedPreferences.getTuitionNotificationToggle()); if (notificationsAreDisabled) return false; - final FeesFetcher feesFetcher = FeesFetcher(); - final DateTime? dueDate = await parseFeesNextLimit( - await feesFetcher.getUserFeesResponse(session)); + final feesFetcher = FeesFetcher(); + final dueDate = parseFeesNextLimit( + await feesFetcher.getUserFeesResponse(session), + ); if (dueDate == null) return false; @@ -55,25 +67,33 @@ class TuitionNotification extends Notification { } @override - void displayNotification(Tuple2 content, - FlutterLocalNotificationsPlugin localNotificationsPlugin) { - const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - "propinas-notificacao", "propinas-notificacao", - importance: Importance.high); + void displayNotification( + Tuple2 content, + FlutterLocalNotificationsPlugin localNotificationsPlugin, + ) { + const androidNotificationDetails = AndroidNotificationDetails( + 'propinas-notificacao', + 'propinas-notificacao', + importance: Importance.high, + ); - const DarwinNotificationDetails darwinNotificationDetails = - DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - interruptionLevel: InterruptionLevel.active); + const darwinNotificationDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + interruptionLevel: InterruptionLevel.active, + ); - const NotificationDetails notificationDetails = NotificationDetails( - android: androidNotificationDetails, - iOS: darwinNotificationDetails, - macOS: darwinNotificationDetails); + const notificationDetails = NotificationDetails( + android: androidNotificationDetails, + iOS: darwinNotificationDetails, + macOS: darwinNotificationDetails, + ); localNotificationsPlugin.show( - 2, content.item1, content.item2, notificationDetails); + 2, + content.item1, + content.item2, + notificationDetails, + ); } } diff --git a/uni/lib/controller/fetchers/calendar_fetcher_html.dart b/uni/lib/controller/fetchers/calendar_fetcher_html.dart index ea4ec32a9..e08119850 100644 --- a/uni/lib/controller/fetchers/calendar_fetcher_html.dart +++ b/uni/lib/controller/fetchers/calendar_fetcher_html.dart @@ -1,4 +1,3 @@ -import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_calendar.dart'; @@ -9,19 +8,17 @@ import 'package:uni/model/entities/session.dart'; class CalendarFetcherHtml implements SessionDependantFetcher { @override List getEndpoints(Session session) { - // TO DO: Implement parsers for all faculties + // TODO(bdmendes): Implement parsers for all faculties // and dispatch for different fetchers - final String url = - '${NetworkRouter.getBaseUrl('feup')}web_base.gera_pagina?p_pagina=página%20estática%20genérica%20106'; + final url = '${NetworkRouter.getBaseUrl('feup')}' + 'web_base.gera_pagina?p_pagina=página%20estática%20genérica%20106'; return [url]; } Future> getCalendar(Session session) async { - final String url = getEndpoints(session)[0]; - final Future response = - NetworkRouter.getWithCookies(url, {}, session); - final List calendar = - await response.then((response) => getCalendarFromHtml(response)); + final url = getEndpoints(session)[0]; + final response = NetworkRouter.getWithCookies(url, {}, session); + final calendar = await response.then(getCalendarFromHtml); return calendar; } } 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 4cb53205e..c6dbdc8e2 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 @@ -6,35 +6,41 @@ import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/session.dart'; class AllCourseUnitsFetcher { - Future> getAllCourseUnitsAndCourseAverages( - List courses, Session session) async { - final List allCourseUnits = []; - for (Course course in courses) { + Future?> getAllCourseUnitsAndCourseAverages( + List courses, + Session session, + ) async { + final allCourseUnits = []; + + for (final course in courses) { try { - final List courseUnits = - await _getAllCourseUnitsAndCourseAveragesFromCourse( - course, session); + final courseUnits = await _getAllCourseUnitsAndCourseAveragesFromCourse( + course, + session, + ); allCourseUnits.addAll(courseUnits.where((c) => c.enrollmentIsValid())); } catch (e) { Logger().e('Failed to fetch course units for ${course.name}', e); + return null; } } + return allCourseUnits; } Future> _getAllCourseUnitsAndCourseAveragesFromCourse( - Course course, Session session) async { - if (course.faculty == null) { - return []; - } - final String url = - '${NetworkRouter.getBaseUrl(course.faculty!)}fest_geral.curso_percurso_academico_view'; + Course course, + Session session, + ) async { + final url = '${NetworkRouter.getBaseUrl(course.faculty!)}' + 'fest_geral.curso_percurso_academico_view'; final response = await NetworkRouter.getWithCookies( - url, - { - 'pv_fest_id': course.festId.toString(), - }, - session); + url, + { + 'pv_fest_id': course.festId.toString(), + }, + session, + ); return parseCourseUnitsAndCourseAverage(response, course); } } 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 f2e39788c..608a23f6c 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -1,5 +1,4 @@ import 'package:html/parser.dart'; -import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_course_unit_info.dart'; @@ -14,43 +13,51 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { } Future fetchCourseUnitSheet( - Session session, int occurrId) async { + Session session, + int occurrId, + ) async { // if course unit is not from the main faculty, Sigarra redirects - final String url = '${getEndpoints(session)[0]}ucurr_geral.ficha_uc_view'; - final Response response = await NetworkRouter.getWithCookies( - url, {'pv_ocorrencia_id': occurrId.toString()}, session); + final url = '${getEndpoints(session)[0]}ucurr_geral.ficha_uc_view'; + final response = await NetworkRouter.getWithCookies( + url, + {'pv_ocorrencia_id': occurrId.toString()}, + session, + ); return parseCourseUnitSheet(response); } Future> fetchCourseUnitClasses( - Session session, int occurrId) async { - List courseUnitClasses = []; + Session session, + int occurrId, + ) async { + var courseUnitClasses = []; - for (String endpoint in getEndpoints(session)) { + for (final endpoint in getEndpoints(session)) { // Crawl classes from all courses that the course unit is offered in - final String courseChoiceUrl = - '${endpoint}it_listagem.lista_cursos_disciplina?pv_ocorrencia_id=$occurrId'; - final Response courseChoiceResponse = + final courseChoiceUrl = '$endpoint' + 'it_listagem.lista_cursos_disciplina?pv_ocorrencia_id=$occurrId'; + final courseChoiceResponse = await NetworkRouter.getWithCookies(courseChoiceUrl, {}, session); final courseChoiceDocument = parse(courseChoiceResponse.body); - final List urls = courseChoiceDocument + final urls = courseChoiceDocument .querySelectorAll('a') - .where((element) => - element.attributes['href'] != null && - element.attributes['href']! - .contains('it_listagem.lista_turma_disciplina')) + .where( + (element) => + element.attributes['href'] != null && + element.attributes['href']! + .contains('it_listagem.lista_turma_disciplina'), + ) .map((e) { - String? url = e.attributes['href']!; + var url = e.attributes['href']!; if (!url.contains('sigarra.up.pt')) { url = endpoint + url; } return url; }).toList(); - for (String url in urls) { + for (final url in urls) { try { - final Response response = - await NetworkRouter.getWithCookies(url, {}, session); + final response = await NetworkRouter.getWithCookies(url, {}, session); courseUnitClasses += parseCourseUnitClasses(response, endpoint); } catch (_) { continue; diff --git a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart index 7160c5369..e4250858a 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/current_course_units_fetcher.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; @@ -10,28 +9,38 @@ class CurrentCourseUnitsFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { // all faculties list user course units on all faculties - final String url = - '${NetworkRouter.getBaseUrlsFromSession(session)[0]}mob_fest_geral.ucurr_inscricoes_corrente'; + final url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}' + 'mob_fest_geral.ucurr_inscricoes_corrente'; return [url]; } Future> getCurrentCourseUnits(Session session) async { - final String url = getEndpoints(session)[0]; - final Response response = await NetworkRouter.getWithCookies( - url, {'pv_codigo': session.username}, session); - if (response.statusCode == 200) { - final responseBody = json.decode(response.body); - final List ucs = []; - for (var course in responseBody) { - for (var uc in course['inscricoes']) { - final CourseUnit? courseUnit = CourseUnit.fromJson(uc); - if (courseUnit != null) { - ucs.add(courseUnit); - } + final url = getEndpoints(session)[0]; + final response = await NetworkRouter.getWithCookies( + url, + {'pv_codigo': session.username}, + session, + ); + + if (response.statusCode != 200) { + return []; + } + + final responseBody = json.decode(response.body) as List; + + final ucs = []; + + for (final course in responseBody) { + final enrollments = + (course as Map)['inscricoes'] as List; + for (final uc in enrollments) { + final courseUnit = CourseUnit.fromJson(uc as Map); + if (courseUnit != null) { + ucs.add(courseUnit); } } - return ucs; } - return []; + + return ucs; } } diff --git a/uni/lib/controller/fetchers/courses_fetcher.dart b/uni/lib/controller/fetchers/courses_fetcher.dart index 2e0f50808..776e042e1 100644 --- a/uni/lib/controller/fetchers/courses_fetcher.dart +++ b/uni/lib/controller/fetchers/courses_fetcher.dart @@ -17,8 +17,13 @@ class CoursesFetcher implements SessionDependantFetcher { List> getCoursesListResponses(Session session) { final urls = getEndpoints(session); return urls - .map((url) => NetworkRouter.getWithCookies( - url, {'pv_num_unico': session.username}, session)) + .map( + (url) => NetworkRouter.getWithCookies( + url, + {'pv_num_unico': session.username}, + session, + ), + ) .toList(); } } diff --git a/uni/lib/controller/fetchers/departures_fetcher.dart b/uni/lib/controller/fetchers/departures_fetcher.dart index 4c63c0211..921c42113 100644 --- a/uni/lib/controller/fetchers/departures_fetcher.dart +++ b/uni/lib/controller/fetchers/departures_fetcher.dart @@ -1,23 +1,24 @@ import 'dart:convert'; +import 'package:html/dom.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart' as http; +import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/bus.dart'; import 'package:uni/model/entities/bus_stop.dart'; -import 'package:http/http.dart' as http; -import 'package:html/parser.dart'; import 'package:uni/model/entities/trip.dart'; -import 'package:uni/controller/networking/network_router.dart'; class DeparturesFetcher { + DeparturesFetcher(this._stopCode, this._stopData); + final String _stopCode; final BusStopData _stopData; - DeparturesFetcher(this._stopCode, this._stopData); - Future _getCSRFToken() async { final url = 'https://www.stcp.pt/en/travel/timetables/?paragem=$_stopCode&t=smsbus'; - final http.Response response = await http.get(url.toUri()); + final response = await http.get(url.toUri()); final htmlResponse = parse(response.body); final scriptText = htmlResponse @@ -32,7 +33,9 @@ class DeparturesFetcher { .firstWhere((element) => element.contains(')')); final csrfToken = callParam.substring( - callParam.indexOf('\'') + 1, callParam.lastIndexOf('\'')); + callParam.indexOf("'") + 1, + callParam.lastIndexOf("'"), + ); return csrfToken; } @@ -47,7 +50,7 @@ class DeparturesFetcher { final url = 'https://www.stcp.pt/pt/itinerarium/soapclient.php?codigo=$_stopCode&hash123=$csrfToken'; - final http.Response response = await http.get(url.toUri()); + final response = await http.get(url.toUri()); final htmlResponse = parse(response.body); final tableEntries = @@ -57,7 +60,7 @@ class DeparturesFetcher { final tripList = []; - for (var entry in tableEntries) { + for (final entry in tableEntries) { final rawBusInformation = entry.querySelectorAll('td'); final busLine = @@ -75,12 +78,13 @@ class DeparturesFetcher { .replaceAll('-', '') .substring(busLine!.length + 1); - final busTimeRemaining = getBusTimeRemaining(rawBusInformation); + final busTimeRemaining = _getBusTimeRemaining(rawBusInformation); - final Trip newTrip = Trip( - line: busLine, - destination: busDestination, - timeRemaining: busTimeRemaining); + final newTrip = Trip( + line: busLine, + destination: busDestination, + timeRemaining: busTimeRemaining, + ); tripList.add(newTrip); } @@ -88,28 +92,31 @@ class DeparturesFetcher { } /// Extracts the time remaining for a bus to reach a stop. - static int getBusTimeRemaining(rawBusInformation) { + static int _getBusTimeRemaining(List rawBusInformation) { if (rawBusInformation[1].text.trim() == 'a passar') { return 0; } else { - final regex = RegExp(r'([0-9]+)'); + final regex = RegExp('([0-9]+)'); - return int.parse(regex.stringMatch(rawBusInformation[2].text).toString()); + return int.parse( + regex.stringMatch(rawBusInformation[2].text).toString(), + ); } } /// Retrieves the name and code of the stops with code [stopCode]. static Future> getStopsByName(String stopCode) async { - final List stopsList = []; + final stopsList = []; - //Search by approximate name - final String url = + // Search by approximate name + final url = 'https://www.stcp.pt/pt/itinerarium/callservice.php?action=srchstoplines&stopname=$stopCode'; - final http.Response response = await http.post(url.toUri()); - final List json = jsonDecode(response.body); - for (var busKey in json) { - final String stop = busKey['name'] + ' [' + busKey['code'] + ']'; - stopsList.add(stop); + final response = await http.post(url.toUri()); + final json = jsonDecode(response.body) as List; + for (final busKey in json) { + final bus = busKey as Map; + final stopDescription = '${bus['name']} [${bus['code']}]'; + stopsList.add(stopDescription); } return stopsList; @@ -117,27 +124,31 @@ class DeparturesFetcher { /// Retrieves real-time information about the user's selected bus lines. static Future> getNextArrivalsStop( - String stopCode, BusStopData stopData) { + String stopCode, + BusStopData stopData, + ) { return DeparturesFetcher(stopCode, stopData).getDepartures(); } /// Returns the bus lines that stop at the given [stop]. static Future> getBusesStoppingAt(String stop) async { - final String url = + final url = 'https://www.stcp.pt/pt/itinerarium/callservice.php?action=srchstoplines&stopcode=$stop'; - final http.Response response = await http.post(url.toUri()); + final response = await http.post(url.toUri()); - final List json = jsonDecode(response.body); + final json = jsonDecode(response.body) as List; - final List buses = []; + final buses = []; - for (var busKey in json) { - final lines = busKey['lines']; - for (var bus in lines) { - final Bus newBus = Bus( - busCode: bus['code'], - destination: bus['description'], - direction: (bus['dir'] == 0 ? false : true)); + for (final data in json) { + final lines = (data as Map)['lines'] as List; + for (final busInfo in lines) { + final bus = busInfo as Map; + final newBus = Bus( + busCode: bus['code'] as String, + destination: bus['description'] as String, + direction: bus['dir'] != 0, + ); buses.add(newBus); } } diff --git a/uni/lib/controller/fetchers/exam_fetcher.dart b/uni/lib/controller/fetchers/exam_fetcher.dart index 4ddd49076..9231eff88 100644 --- a/uni/lib/controller/fetchers/exam_fetcher.dart +++ b/uni/lib/controller/fetchers/exam_fetcher.dart @@ -7,9 +7,9 @@ import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/entities/session.dart'; class ExamFetcher implements SessionDependantFetcher { + ExamFetcher(this.courses, this.userUcs); List courses; List userUcs; - ExamFetcher(this.courses, this.userUcs); @override List getEndpoints(Session session) { @@ -20,24 +20,31 @@ class ExamFetcher implements SessionDependantFetcher { } Future> extractExams( - Session session, ParserExams parserExams) async { - Set courseExams = {}; + Session session, + ParserExams parserExams, + ) async { + var courseExams = {}; final urls = getEndpoints(session); - for (Course course in courses) { + for (final course in courses) { for (final url in urls) { - final Set currentCourseExams = await parserExams.parseExams( - await NetworkRouter.getWithCookies( - url, {'p_curso_id': course.id.toString()}, session), - course); + final currentCourseExams = await parserExams.parseExams( + await NetworkRouter.getWithCookies( + url, + {'p_curso_id': course.id.toString()}, + session, + ), + course, + ); courseExams = Set.from(courseExams)..addAll(currentCourseExams); } } - final Set exams = {}; - for (Exam courseExam in courseExams) { - for (CourseUnit uc in userUcs) { + final exams = {}; + for (final courseExam in courseExams) { + for (final uc in userUcs) { if (!courseExam.type.contains( - '''Exames ao abrigo de estatutos especiais - Port.Est.Especiais''') && + '''Exames ao abrigo de estatutos especiais - Port.Est.Especiais''', + ) && courseExam.type != 'EE' && courseExam.type != 'EAE' && courseExam.subject == uc.abbreviation && diff --git a/uni/lib/controller/fetchers/fees_fetcher.dart b/uni/lib/controller/fetchers/fees_fetcher.dart index edd225aae..c08a35164 100644 --- a/uni/lib/controller/fetchers/fees_fetcher.dart +++ b/uni/lib/controller/fetchers/fees_fetcher.dart @@ -8,14 +8,14 @@ class FeesFetcher implements SessionDependantFetcher { List getEndpoints(Session session) { // TO DO: Check balance on all faculties and discard if user is not enrolled // Some shared courses (such as L.EIC) do not put fees on both faculties - final url = - '${NetworkRouter.getBaseUrlsFromSession(session)[0]}gpag_ccorrente_geral.conta_corrente_view'; + final url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}' + 'gpag_ccorrente_geral.conta_corrente_view'; return [url]; } Future getUserFeesResponse(Session session) { - final String url = getEndpoints(session)[0]; - final Map query = {'pct_cod': session.username}; + final url = getEndpoints(session)[0]; + final query = {'pct_cod': session.username}; return NetworkRouter.getWithCookies(url, query, session); } } diff --git a/uni/lib/controller/fetchers/library_occupation_fetcher.dart b/uni/lib/controller/fetchers/library_occupation_fetcher.dart index eb0c989b2..1c7efae69 100644 --- a/uni/lib/controller/fetchers/library_occupation_fetcher.dart +++ b/uni/lib/controller/fetchers/library_occupation_fetcher.dart @@ -1,4 +1,3 @@ -import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_library_occupation.dart'; @@ -9,22 +8,19 @@ import 'package:uni/model/entities/session.dart'; class LibraryOccupationFetcherSheets implements SessionDependantFetcher { @override List getEndpoints(Session session) { - // TODO:: Implement parsers for all faculties - // and dispatch for different fetchers - const String baseUrl = 'https://docs.google.com/spreadsheets/d/'; - const String sheetId = '1gZRbEX4y8vNW7vrl15FCdAQ3pVNRJw_uRZtVL6ORP0g'; - const String url = + const baseUrl = 'https://docs.google.com/spreadsheets/d/'; + const sheetId = '1gZRbEX4y8vNW7vrl15FCdAQ3pVNRJw_uRZtVL6ORP0g'; + const url = '$baseUrl$sheetId/gviz/tq?tqx=out:json&sheet=MANUAL&range=C2:E7&tq=SELECT+C,E'; return [url]; } Future getLibraryOccupationFromSheets( - Session session) async { - final String url = getEndpoints(session)[0]; - final Future response = - NetworkRouter.getWithCookies(url, {}, session); - final LibraryOccupation occupation = await response - .then((response) => parseLibraryOccupationFromSheets(response)); + Session session, + ) async { + final url = getEndpoints(session)[0]; + final response = NetworkRouter.getWithCookies(url, {}, session); + final occupation = await response.then(parseLibraryOccupationFromSheets); return occupation; } } diff --git a/uni/lib/controller/fetchers/location_fetcher/location_fetcher.dart b/uni/lib/controller/fetchers/location_fetcher/location_fetcher.dart index e464ce430..6bbdf2623 100644 --- a/uni/lib/controller/fetchers/location_fetcher/location_fetcher.dart +++ b/uni/lib/controller/fetchers/location_fetcher/location_fetcher.dart @@ -8,27 +8,36 @@ abstract class LocationFetcher { Future> getLocations(); Future> getFromJSON(String jsonStr) async { - final Map json = jsonDecode(jsonStr); - final List groupsMap = json['data']; - final List groups = []; + final json = jsonDecode(jsonStr) as Map; + final groupsMap = json['data'] as List; + final groups = []; - for (Map groupMap in groupsMap) { - final int id = groupMap['id']; - final double lat = groupMap['lat']; - final double lng = groupMap['lng']; - final bool isFloorless = groupMap['isFloorless']; + for (final groupMap in groupsMap) { + final map = groupMap as Map; + final id = map['id'] as int; + final lat = map['lat'] as double; + final lng = map['lng'] as double; + final isFloorless = map['isFloorless'] as bool; - final Map locationsMap = groupMap['locations']; + final locationsMap = map['locations'] as Map; - final List locations = []; + final locations = []; locationsMap.forEach((key, value) { - final int floor = int.parse(key); - value.forEach((locationJson) { - locations.add(Location.fromJSON(locationJson, floor)); - }); + final floor = int.parse(key); + for (final locationJson in value as List) { + locations.add( + Location.fromJSON(locationJson as Map, floor), + ); + } }); - groups.add(LocationGroup(LatLng(lat, lng), - locations: locations, isFloorless: isFloorless, id: id)); + groups.add( + LocationGroup( + LatLng(lat, lng), + locations: locations, + isFloorless: isFloorless, + id: id, + ), + ); } return groups; diff --git a/uni/lib/controller/fetchers/location_fetcher/location_fetcher_asset.dart b/uni/lib/controller/fetchers/location_fetcher/location_fetcher_asset.dart index cf2249986..621adf857 100644 --- a/uni/lib/controller/fetchers/location_fetcher/location_fetcher_asset.dart +++ b/uni/lib/controller/fetchers/location_fetcher/location_fetcher_asset.dart @@ -5,8 +5,7 @@ import 'package:uni/model/entities/location_group.dart'; class LocationFetcherAsset extends LocationFetcher { @override Future> getLocations() async { - final String json = - await rootBundle.loadString('assets/text/locations/feup.json'); + final json = await rootBundle.loadString('assets/text/locations/feup.json'); return getFromJSON(json); } } diff --git a/uni/lib/controller/fetchers/print_fetcher.dart b/uni/lib/controller/fetchers/print_fetcher.dart index 9907dac03..e48e46d35 100644 --- a/uni/lib/controller/fetchers/print_fetcher.dart +++ b/uni/lib/controller/fetchers/print_fetcher.dart @@ -6,32 +6,34 @@ import 'package:uni/model/entities/session.dart'; class PrintFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { - final url = - '${NetworkRouter.getBaseUrl('feup')}imp4_impressoes.atribs'; // endpoint only available for feup + final url = '${NetworkRouter.getBaseUrl('feup')}imp4_impressoes.atribs'; + // endpoint only available for feup return [url]; } - getUserPrintsResponse(Session session) { - final String url = getEndpoints(session)[0]; - final Map query = {'p_codigo': session.username}; + Future getUserPrintsResponse(Session session) { + final url = getEndpoints(session)[0]; + final query = {'p_codigo': session.username}; return NetworkRouter.getWithCookies(url, query, session); } - static Future generatePrintMoneyReference( - double amount, Session session) async { + static Future generatePrintMoneyReference( + double amount, + Session session, + ) async { if (amount < 1.0) return Future.error('Amount less than 1,00€'); - final url = - '${NetworkRouter.getBaseUrlsFromSession(session)[0]}gpag_ccorrentes_geral.gerar_mb'; + final url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}' + 'gpag_ccorrentes_geral.gerar_mb'; - final Map data = { + final data = { 'p_tipo_id': '3', 'pct_codigo': session.username, 'p_valor': '1', 'p_valor_livre': amount.toStringAsFixed(2).trim().replaceAll('.', ',') }; - final Map headers = {}; + final headers = {}; headers['cookie'] = session.cookies; headers['content-type'] = 'application/x-www-form-urlencoded'; diff --git a/uni/lib/controller/fetchers/profile_fetcher.dart b/uni/lib/controller/fetchers/profile_fetcher.dart index d15a4ef1d..56a239d64 100644 --- a/uni/lib/controller/fetchers/profile_fetcher.dart +++ b/uni/lib/controller/fetchers/profile_fetcher.dart @@ -1,10 +1,8 @@ -import 'package:http/http.dart'; import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/courses_fetcher.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_courses.dart'; -import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; @@ -12,41 +10,47 @@ class ProfileFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { final url = NetworkRouter.getBaseUrlsFromSession( - session)[0]; // user profile is the same on all faculties + session, + )[0]; // user profile is the same on all faculties return [url]; } /// Returns the user's [Profile]. - static Future getProfile(Session session) async { - final url = - '${NetworkRouter.getBaseUrlsFromSession(session)[0]}mob_fest_geral.perfil?'; + static Future fetchProfile(Session session) async { + final url = '${NetworkRouter.getBaseUrlsFromSession(session)[0]}' + 'mob_fest_geral.perfil?'; final response = await NetworkRouter.getWithCookies( - url, {'pv_codigo': session.username}, session); + url, + {'pv_codigo': session.username}, + session, + ); - if (response.statusCode == 200) { - final Profile profile = Profile.fromResponse(response); - try { - final List> coursesResponses = - CoursesFetcher().getCoursesListResponses(session); - final List courses = - parseMultipleCourses(await Future.wait(coursesResponses)); - for (Course course in courses) { - if (profile.courses - .map((c) => c.festId) - .toList() - .contains(course.festId)) { - final Course matchingCourse = - profile.courses.where((c) => c.festId == course.festId).first; - matchingCourse.state ??= course.state; - continue; - } - profile.courses.add(course); + if (response.statusCode != 200) { + return null; + } + + final profile = Profile.fromResponse(response); + try { + final coursesResponses = await Future.wait( + CoursesFetcher().getCoursesListResponses(session), + ); + final courses = parseMultipleCourses(coursesResponses); + for (final course in courses) { + if (profile.courses + .map((c) => c.festId) + .toList() + .contains(course.festId)) { + profile.courses + .where((c) => c.festId == course.festId) + .first + .state ??= course.state; + continue; } - } catch (e) { - Logger().e('Failed to get user courses via scrapping: $e'); + profile.courses.add(course); } - return profile; + } catch (e) { + Logger().e('Failed to get user courses via scrapping: $e'); } - return Profile(); + return profile; } } diff --git a/uni/lib/controller/fetchers/reference_fetcher.dart b/uni/lib/controller/fetchers/reference_fetcher.dart index f1258cc6a..3179bff82 100644 --- a/uni/lib/controller/fetchers/reference_fetcher.dart +++ b/uni/lib/controller/fetchers/reference_fetcher.dart @@ -6,19 +6,18 @@ import 'package:uni/model/entities/session.dart'; class ReferenceFetcher implements SessionDependantFetcher { @override List getEndpoints(Session session) { - final List baseUrls = - NetworkRouter.getBaseUrlsFromSession(session) + - [NetworkRouter.getBaseUrl('sasup')]; - final List urls = baseUrls + final baseUrls = NetworkRouter.getBaseUrlsFromSession(session) + + [NetworkRouter.getBaseUrl('sasup')]; + final urls = baseUrls .map((url) => '${url}gpag_ccorrente_geral.conta_corrente_view') .toList(); return urls; } Future getUserReferenceResponse(Session session) { - final List urls = getEndpoints(session); - final String url = urls[0]; - final Map query = {'pct_cod': session.username}; + final urls = getEndpoints(session); + final url = urls[0]; + final query = {'pct_cod': session.username}; return NetworkRouter.getWithCookies(url, query, session); } } diff --git a/uni/lib/controller/fetchers/restaurant_fetcher.dart b/uni/lib/controller/fetchers/restaurant_fetcher.dart index d089a0ce4..deb0cf0c3 100644 --- a/uni/lib/controller/fetchers/restaurant_fetcher.dart +++ b/uni/lib/controller/fetchers/restaurant_fetcher.dart @@ -1,9 +1,7 @@ -import 'package:http/http.dart'; -import 'package:uni/model/entities/restaurant.dart'; -import 'package:uni/model/entities/session.dart'; - import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_restaurants.dart'; +import 'package:uni/model/entities/restaurant.dart'; +import 'package:uni/model/entities/session.dart'; /// Class for fetching the menu class RestaurantFetcher { @@ -13,7 +11,7 @@ class RestaurantFetcher { // Format: Date(dd/mm/yyyy), Meal("Almoço", "Jantar), Dish("Sopa", "Carne", // "Peixe", "Dieta", "Vegetariano", "Salada"), Description(String) - final String sheetsColumnRange = "A:D"; + final String sheetsColumnRange = 'A:D'; // List the Restaurant sheet names in the Google Sheets Document final List restaurantSheets = ['Cantina']; @@ -21,22 +19,29 @@ class RestaurantFetcher { // Generate the Gsheets endpoints list based on a list of sheets String buildGSheetsEndpoint(String sheet) { return Uri.encodeFull( - "$spreadSheetUrl$jsonEndpoint&sheet=$sheet&range=$sheetsColumnRange"); + '$spreadSheetUrl$jsonEndpoint&sheet=$sheet&range=$sheetsColumnRange', + ); } String getRestaurantGSheetName(Restaurant restaurant) { return restaurantSheets.firstWhere( - (sheetName) => - restaurant.name.toLowerCase().contains(sheetName.toLowerCase()), - orElse: () => ''); + (sheetName) => + restaurant.name.toLowerCase().contains(sheetName.toLowerCase()), + orElse: () => '', + ); } Future fetchGSheetsRestaurant( - String url, String restaurantName, session, - {isDinner = false}) async { + String url, + String restaurantName, + Session session, { + bool isDinner = false, + }) async { return getRestaurantFromGSheets( - await NetworkRouter.getWithCookies(url, {}, session), restaurantName, - isDinner: isDinner); + await NetworkRouter.getWithCookies(url, {}, session), + restaurantName, + isDinner: isDinner, + ); } final List sigarraMenuEndpoints = [ @@ -44,13 +49,13 @@ class RestaurantFetcher { ]; Future> fetchSigarraRestaurants(Session session) async { - final List restaurants = []; + final restaurants = []; - final Iterable> responses = sigarraMenuEndpoints + final responses = sigarraMenuEndpoints .map((url) => NetworkRouter.getWithCookies(url, {}, session)); await Future.wait(responses).then((value) { - for (var response in value) { + for (final response in value) { restaurants.addAll(getRestaurantsFromHtml(response)); } }); @@ -59,26 +64,31 @@ class RestaurantFetcher { } Future> getRestaurants(Session session) async { - final List restaurants = await fetchSigarraRestaurants(session); + final restaurants = await fetchSigarraRestaurants(session); - // Check for restaurants without associated meals and attempt to parse them from GSheets - final List restaurantsWithoutMeals = + // Check for restaurants without associated meals and attempt to parse them + // from GSheets + final restaurantsWithoutMeals = restaurants.where((restaurant) => restaurant.meals.isEmpty).toList(); - for (var restaurant in restaurantsWithoutMeals) { + for (final restaurant in restaurantsWithoutMeals) { final sheetName = getRestaurantGSheetName(restaurant); if (sheetName.isEmpty) { continue; } - final Restaurant gSheetsRestaurant = await fetchGSheetsRestaurant( - buildGSheetsEndpoint(sheetName), restaurant.name, session, - isDinner: restaurant.name.toLowerCase().contains('jantar')); - - restaurants.removeWhere( - (restaurant) => restaurant.name == gSheetsRestaurant.name); - - restaurants.insert(0, gSheetsRestaurant); + final gSheetsRestaurant = await fetchGSheetsRestaurant( + buildGSheetsEndpoint(sheetName), + restaurant.name, + session, + isDinner: restaurant.name.toLowerCase().contains('jantar'), + ); + + restaurants + ..removeWhere( + (restaurant) => restaurant.name == gSheetsRestaurant.name, + ) + ..insert(0, gSheetsRestaurant); } return restaurants; diff --git a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart index 3990f3ba8..b9e6a8cce 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher.dart @@ -12,12 +12,12 @@ abstract class ScheduleFetcher extends SessionDependantFetcher { Dates getDates() { var date = DateTime.now(); - final String beginWeek = date.year.toString().padLeft(4, '0') + + 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)); - final String endWeek = date.year.toString().padLeft(4, '0') + + final endWeek = date.year.toString().padLeft(4, '0') + date.month.toString().padLeft(2, '0') + date.day.toString().padLeft(2, '0'); @@ -28,9 +28,8 @@ abstract class ScheduleFetcher extends SessionDependantFetcher { /// 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; final int lectiveYear; - - Dates(this.beginWeek, this.endWeek, this.lectiveYear); } 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 71b60501f..24f8e4346 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_api.dart @@ -1,3 +1,4 @@ +import 'package:http/http.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_schedule.dart'; @@ -20,18 +21,19 @@ class ScheduleFetcherApi extends ScheduleFetcher { Future> getLectures(Session session, Profile profile) async { final dates = getDates(); final urls = getEndpoints(session); - final responses = []; - for (var url in urls) { + final responses = []; + 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); + url, + { + 'pv_codigo': session.username, + 'pv_semana_ini': dates.beginWeek, + 'pv_semana_fim': dates.endWeek + }, + session, + ); responses.add(response); } - return await parseScheduleMultipleRequests(responses); + 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 b78cdd7d5..6d9594a4b 100644 --- a/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart +++ b/uni/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_html.dart @@ -21,25 +21,27 @@ class ScheduleFetcherHtml extends ScheduleFetcher { Future> getLectures(Session session, Profile profile) async { final dates = getDates(); final urls = getEndpoints(session); - final List lectureResponses = []; + final lectureResponses = []; for (final course in profile.courses) { for (final url in urls) { 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); + url, + { + 'pv_fest_id': course.festId.toString(), + 'pv_ano_lectivo': dates.lectiveYear.toString(), + 'p_semana_inicio': dates.beginWeek, + 'p_semana_fim': dates.endWeek + }, + session, + ); lectureResponses.add(response); } } - final List lectures = await Future.wait(lectureResponses - .map((response) => getScheduleFromHtml(response, session))) - .then((schedules) => schedules.expand((schedule) => schedule).toList()); + final lectures = await Future.wait( + lectureResponses + .map((response) => getScheduleFromHtml(response, session)), + ).then((schedules) => schedules.expand((schedule) => schedule).toList()); lectures.sort((l1, l2) => l1.compare(l2)); return lectures; diff --git a/uni/lib/controller/load_static/terms_and_conditions.dart b/uni/lib/controller/load_static/terms_and_conditions.dart index dd1cecb17..ba6945175 100644 --- a/uni/lib/controller/load_static/terms_and_conditions.dart +++ b/uni/lib/controller/load_static/terms_and_conditions.dart @@ -14,29 +14,31 @@ import 'package:uni/controller/local_storage/app_shared_preferences.dart'; Future fetchTermsAndConditions() async { if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { try { - const String url = 'https://raw.githubusercontent.com/NIAEFEUP/' - 'uni/develop/uni/assets/text/TermsAndConditions.md'; - final http.Response response = await http.get(Uri.parse(url)); + 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; } } catch (e) { - Logger().e('Failed to fetch Terms and Conditions: ${e.toString()}'); + Logger().e('Failed to fetch Terms and Conditions: $e'); } } try { return await rootBundle.loadString('assets/text/TermsAndConditions.md'); } catch (e) { - Logger().e('Failed to read Terms and Conditions: ${e.toString()}'); + Logger().e('Failed to read Terms and Conditions: $e'); return 'Não foi possível carregar os Termos e Condições. ' 'Por favor tente mais tarde.'; } } /// Checks if the current Terms and Conditions have been accepted by the user, -/// by fetching the current terms, hashing them and comparing with the stored hash. -/// Sets the acceptance to false if the terms have changed, or true if they haven't. +/// by fetching the current terms, hashing them and comparing +/// with the stored hash. +/// Sets the acceptance to false if the terms have changed, +/// or true if they haven't. /// Returns the updated value. Future updateTermsAndConditionsAcceptancePreference() async { final hash = await AppSharedPreferences.getTermsAndConditionHash(); @@ -44,18 +46,22 @@ Future updateTermsAndConditionsAcceptancePreference() async { final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); if (hash == null) { - await AppSharedPreferences.setTermsAndConditionsAcceptance(true); + await AppSharedPreferences.setTermsAndConditionsAcceptance( + areAccepted: true, + ); await AppSharedPreferences.setTermsAndConditionHash(currentHash); return true; } if (currentHash != hash) { - await AppSharedPreferences.setTermsAndConditionsAcceptance(false); + await AppSharedPreferences.setTermsAndConditionsAcceptance( + areAccepted: false, + ); await AppSharedPreferences.setTermsAndConditionHash(currentHash); return false; } - await AppSharedPreferences.setTermsAndConditionsAcceptance(true); + await AppSharedPreferences.setTermsAndConditionsAcceptance(areAccepted: true); return true; } @@ -64,5 +70,5 @@ Future acceptTermsAndConditions() async { final termsAndConditions = await fetchTermsAndConditions(); final currentHash = md5.convert(utf8.encode(termsAndConditions)).toString(); await AppSharedPreferences.setTermsAndConditionHash(currentHash); - await AppSharedPreferences.setTermsAndConditionsAcceptance(true); + await AppSharedPreferences.setTermsAndConditionsAcceptance(areAccepted: true); } diff --git a/uni/lib/controller/local_storage/app_bus_stop_database.dart b/uni/lib/controller/local_storage/app_bus_stop_database.dart index e13fdbe92..a62476004 100644 --- a/uni/lib/controller/local_storage/app_bus_stop_database.dart +++ b/uni/lib/controller/local_storage/app_bus_stop_database.dart @@ -24,7 +24,7 @@ class AppBusStopDatabase extends AppDatabase { /// * a value in this map is the corresponding [BusStopData] instance. Future> busStops() async { // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); // Query the table for all bus stops final List> buses = await db.query('busstops'); @@ -32,23 +32,28 @@ class AppBusStopDatabase extends AppDatabase { final List> favoritesQueryResult = await db.query('favoritestops'); - final Map favorites = {}; - for (var e in favoritesQueryResult) { - favorites[e['stopCode']] = e['favorited'] == '1'; + final favorites = {}; + for (final station in favoritesQueryResult) { + favorites[station['stopCode'] as String] = station['favorited'] == '1'; } - final Map stops = {}; - groupBy(buses, (stop) => (stop! as dynamic)['stopCode']).forEach( - (stopCode, busCodeList) => stops[stopCode] = BusStopData( - configuredBuses: Set.from( - busCodeList.map((busEntry) => busEntry['busCode'])), - favorited: favorites[stopCode]!)); + final stops = {}; + groupBy(buses, (stop) => (stop! as Map)['stopCode']) + .forEach( + (stopCode, busCodeList) => stops[stopCode as String] = BusStopData( + configuredBuses: Set.from( + busCodeList.map((busEntry) => busEntry['busCode']), + ), + favorited: favorites[stopCode]!, + ), + ); + return stops; } /// Toggles whether or not a bus stop is considered a user's favorite. Future updateFavoriteBusStop(String stopCode) async { - final Map stops = await busStops(); + final stops = await busStops(); stops[stopCode]!.favorited = !stops[stopCode]!.favorited; await deleteBusStops(); await _insertBusStops(stops); @@ -56,7 +61,7 @@ class AppBusStopDatabase extends AppDatabase { /// Adds a bus stop to this database. Future addBusStop(String stopCode, BusStopData stopData) async { - final Map stops = await busStops(); + final stops = await busStops(); stops[stopCode] = stopData; await deleteBusStops(); await _insertBusStops(stops); @@ -64,7 +69,7 @@ class AppBusStopDatabase extends AppDatabase { /// Removes a bus stop from this database. Future removeBusStop(String stopCode) async { - final Map stops = await busStops(); + final stops = await busStops(); stops.remove(stopCode); await deleteBusStops(); await _insertBusStops(stops); @@ -74,10 +79,12 @@ class AppBusStopDatabase extends AppDatabase { /// /// If a row with the same data is present, it will be replaced. Future _insertBusStops(Map stops) async { - stops.forEach((stopCode, stopData) async { - await insertInDatabase('favoritestops', - {'stopCode': stopCode, 'favorited': stopData.favorited ? '1' : '0'}); - for (var busCode in stopData.configuredBuses) { + stops.forEach((String stopCode, BusStopData stopData) async { + await insertInDatabase( + 'favoritestops', + {'stopCode': stopCode, 'favorited': stopData.favorited ? '1' : '0'}, + ); + for (final busCode in stopData.configuredBuses) { await insertInDatabase( 'busstops', { @@ -92,8 +99,7 @@ class AppBusStopDatabase extends AppDatabase { /// Deletes all of the bus stops from this database. Future deleteBusStops() async { - // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('busstops'); } diff --git a/uni/lib/controller/local_storage/app_calendar_database.dart b/uni/lib/controller/local_storage/app_calendar_database.dart index 5c0e8660e..b8674e2b9 100644 --- a/uni/lib/controller/local_storage/app_calendar_database.dart +++ b/uni/lib/controller/local_storage/app_calendar_database.dart @@ -1,4 +1,3 @@ -import 'package:sqflite/sqflite.dart'; import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/calendar_event.dart'; @@ -12,11 +11,11 @@ class CalendarDatabase extends AppDatabase { date TEXT)''' ]); - void saveCalendar(List calendar) async { - final Database db = await getDatabase(); - db.transaction((txn) async { + Future saveCalendar(List calendar) async { + final db = await getDatabase(); + await db.transaction((txn) async { await txn.delete('CALENDAR'); - for (var event in calendar) { + for (final event in calendar) { await txn.insert('CALENDAR', event.toMap()); } }); @@ -24,12 +23,15 @@ class CalendarDatabase extends AppDatabase { //Returns a list with all calendar events stored in the database Future> calendar() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('calendar'); return List.generate(maps.length, (i) { - return CalendarEvent(maps[i]['name'], maps[i]['date']); + return CalendarEvent( + maps[i]['name'] as String, + maps[i]['date'] as String, + ); }); } } diff --git a/uni/lib/controller/local_storage/app_course_units_database.dart b/uni/lib/controller/local_storage/app_course_units_database.dart index 29bb850dc..c9968845a 100644 --- a/uni/lib/controller/local_storage/app_course_units_database.dart +++ b/uni/lib/controller/local_storage/app_course_units_database.dart @@ -5,45 +5,45 @@ import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; class AppCourseUnitsDatabase extends AppDatabase { + AppCourseUnitsDatabase() : super('course_units.db', [createScript]); static const String createScript = - '''CREATE TABLE course_units(id INTEGER, code TEXT, abbreviation TEXT,''' - '''name TEXT, curricularYear INTEGER, occurrId INTEGER, semesterCode TEXT,''' - '''semesterName TEXT, type TEXT, status TEXT, grade TEXT, ectsGrade TEXT,''' + '''CREATE TABLE course_units(id INTEGER, code TEXT, abbreviation TEXT , ''' + '''name TEXT, curricularYear INTEGER, occurrId INTEGER, semesterCode TEXT, ''' + '''semesterName TEXT, type TEXT, status TEXT, grade TEXT, ectsGrade TEXT, ''' '''result TEXT, ects REAL, schoolYear TEXT)'''; - AppCourseUnitsDatabase() : super('course_units.db', [createScript]); - saveNewCourseUnits(List courseUnits) async { + Future saveNewCourseUnits(List courseUnits) async { await deleteCourseUnits(); await _insertCourseUnits(courseUnits); } Future> courseUnits() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('course_units'); return List.generate(maps.length, (i) { return CourseUnit( - id: maps[i]['id'], - code: maps[i]['code'], - abbreviation: maps[i]['abbreviation'], - name: maps[i]['name'], - curricularYear: maps[i]['curricularYear'], - occurrId: maps[i]['occurrId'], - semesterCode: maps[i]['semesterCode'], - semesterName: maps[i]['semesterName'], - type: maps[i]['type'], - status: maps[i]['status'], - grade: maps[i]['grade'], - ectsGrade: maps[i]['ectsGrade'], - result: maps[i]['result'], - ects: maps[i]['ects'], - schoolYear: maps[i]['schoolYear'], + id: maps[i]['id'] as int, + code: maps[i]['code'] as String, + abbreviation: maps[i]['abbreviation'] as String, + name: maps[i]['name'] as String, + curricularYear: maps[i]['curricularYear'] as int?, + occurrId: maps[i]['occurrId'] as int, + semesterCode: maps[i]['semesterCode'] as String?, + semesterName: maps[i]['semesterName'] as String?, + type: maps[i]['type'] as String?, + status: maps[i]['status'] as String?, + grade: maps[i]['grade'] as String?, + ectsGrade: maps[i]['ectsGrade'] as String?, + result: maps[i]['result'] as String?, + ects: maps[i]['ects'] as double?, + schoolYear: maps[i]['schoolYear'] as String?, ); }); } Future _insertCourseUnits(List courseUnits) async { - for (CourseUnit courseUnit in courseUnits) { + for (final courseUnit in courseUnits) { await insertInDatabase( 'course_units', courseUnit.toMap(), @@ -53,7 +53,7 @@ class AppCourseUnitsDatabase extends AppDatabase { } Future deleteCourseUnits() async { - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('course_units'); } } diff --git a/uni/lib/controller/local_storage/app_courses_database.dart b/uni/lib/controller/local_storage/app_courses_database.dart index f85e35b44..f9130e050 100644 --- a/uni/lib/controller/local_storage/app_courses_database.dart +++ b/uni/lib/controller/local_storage/app_courses_database.dart @@ -9,37 +9,38 @@ import 'package:uni/model/entities/course.dart'; /// This database stores information about the user's courses. /// See the [Course] class to see what data is stored in this database. class AppCoursesDatabase extends AppDatabase { - static const String createScript = - '''CREATE TABLE courses(id INTEGER, fest_id INTEGER, name TEXT,''' - '''abbreviation TEXT, currYear TEXT, firstEnrollment INTEGER, state TEXT,''' - '''faculty TEXT, currentAverage REAL, finishedEcts REAL)'''; AppCoursesDatabase() : super('courses.db', [createScript], onUpgrade: migrate, version: 2); + static const String createScript = + '''CREATE TABLE courses(id INTEGER, fest_id INTEGER, name TEXT, ''' + '''abbreviation TEXT, currYear TEXT, firstEnrollment INTEGER, state TEXT, ''' + '''faculty TEXT, currentAverage REAL, finishedEcts REAL)'''; /// Replaces all of the data in this database with the data from [courses]. - saveNewCourses(List courses) async { + Future saveNewCourses(List courses) async { await deleteCourses(); await _insertCourses(courses); } /// Returns a list containing all of the courses stored in this database. Future> courses() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('courses'); // Convert the List into a List. return List.generate(maps.length, (i) { return Course( - id: maps[i]['id'] ?? 0, - festId: maps[i]['fest_id'], - name: maps[i]['name'], - abbreviation: maps[i]['abbreviation'], - currYear: maps[i]['currYear'], - firstEnrollment: maps[i]['firstEnrollment'], - state: maps[i]['state'], - faculty: maps[i]['faculty'], - finishedEcts: maps[i]['finishedEcts'], - currentAverage: maps[i]['currentAverage']); + id: maps[i]['id'] as int? ?? 0, + festId: maps[i]['fest_id'] as int? ?? 0, + name: maps[i]['name'] as String?, + abbreviation: maps[i]['abbreviation'] as String?, + currYear: maps[i]['currYear'] as String?, + firstEnrollment: maps[i]['firstEnrollment'] as int? ?? 0, + state: maps[i]['state'] as String?, + faculty: maps[i]['faculty'] as String?, + finishedEcts: maps[i]['finishedEcts'] as double? ?? 0, + currentAverage: maps[i]['currentAverage'] as double? ?? 0, + ); }); } @@ -47,7 +48,7 @@ class AppCoursesDatabase extends AppDatabase { /// /// If a row with the same data is present, it will be replaced. Future _insertCourses(List courses) async { - for (Course course in courses) { + for (final course in courses) { await insertInDatabase( 'courses', course.toMap(), @@ -58,7 +59,7 @@ class AppCoursesDatabase extends AppDatabase { /// Deletes all of the data stored in this database. Future deleteCourses() async { - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('courses'); } @@ -67,10 +68,13 @@ class AppCoursesDatabase extends AppDatabase { /// *Note:* This operation only updates the schema of the tables present in /// the database and, as such, all data is lost. static FutureOr migrate( - Database db, int oldVersion, int newVersion) async { - final batch = db.batch(); - batch.execute('DROP TABLE IF EXISTS courses'); - batch.execute(createScript); + Database db, + int oldVersion, + int newVersion, + ) async { + final batch = db.batch() + ..execute('DROP TABLE IF EXISTS courses') + ..execute(createScript); await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_database.dart b/uni/lib/controller/local_storage/app_database.dart index 1db3a3a04..c24800d5a 100644 --- a/uni/lib/controller/local_storage/app_database.dart +++ b/uni/lib/controller/local_storage/app_database.dart @@ -1,14 +1,16 @@ import 'dart:async'; + import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; -import 'dart:io'; import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; import 'package:synchronized/synchronized.dart'; /// Manages a generic database. /// /// This class is the foundation for all other database managers. class AppDatabase { + AppDatabase(this.name, this.commands, {this.onUpgrade, this.version = 1}); + /// An instance of this database. Database? _db; @@ -17,6 +19,7 @@ class AppDatabase { /// A list of commands to be executed on database creation. List commands; + // A lock that synchronizes all database insertions. static Lock lock = Lock(); @@ -26,8 +29,6 @@ class AppDatabase { /// The version of this database. final int version; - AppDatabase(this.name, this.commands, {this.onUpgrade, this.version = 1}); - /// Returns an instance of this database. Future getDatabase() async { _db ??= await initializeDatabase(); @@ -35,39 +36,51 @@ class AppDatabase { } /// Inserts [values] into the corresponding [table] in this database. - insertInDatabase(String table, Map values, - {String? nullColumnHack, ConflictAlgorithm? conflictAlgorithm}) async { - lock.synchronized(() async { - final Database db = await getDatabase(); + Future insertInDatabase( + String table, + Map values, { + String? nullColumnHack, + ConflictAlgorithm? conflictAlgorithm, + }) async { + await lock.synchronized(() async { + final db = await getDatabase(); - db.insert(table, values, - nullColumnHack: nullColumnHack, conflictAlgorithm: conflictAlgorithm); + await db.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); }); } /// Initializes this database. Future initializeDatabase() async { // Get the directory path for both Android and iOS to store database - final String directory = await getDatabasesPath(); - final String path = join(directory, name); + final directory = await getDatabasesPath(); + final path = join(directory, name); // Open or create the database at the given path - final appDatabase = await openDatabase(path, - version: version, onCreate: _createDatabase, onUpgrade: onUpgrade); + final appDatabase = await openDatabase( + path, + version: version, + onCreate: _createDatabase, + onUpgrade: onUpgrade, + ); return appDatabase; } /// Executes the commands present in [commands]. - void _createDatabase(Database db, int newVersion) async { - for (String command in commands) { + Future _createDatabase(Database db, int newVersion) async { + for (final command in commands) { await db.execute(command); } } /// Removes the database called [name]. - static removeDatabase(String name) async { - final Directory directory = await getApplicationDocumentsDirectory(); - final String path = directory.path + name; + static Future removeDatabase(String name) async { + final directory = await getApplicationDocumentsDirectory(); + final path = directory.path + name; await deleteDatabase(path); } diff --git a/uni/lib/controller/local_storage/app_exams_database.dart b/uni/lib/controller/local_storage/app_exams_database.dart index 1a77730af..f56ae0a0a 100644 --- a/uni/lib/controller/local_storage/app_exams_database.dart +++ b/uni/lib/controller/local_storage/app_exams_database.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:sqflite/sqflite.dart'; import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/exam.dart'; @@ -8,7 +9,9 @@ import 'package:uni/model/entities/exam.dart'; /// This database stores information about the user's exams. /// See the [Exam] class to see what data is stored in this database. class AppExamsDatabase extends AppDatabase { - var months = { + AppExamsDatabase() + : super('exams.db', [_createScript], onUpgrade: migrate, version: 4); + Map months = { 'Janeiro': '01', 'Fevereiro': '02', 'Março': '03', @@ -23,33 +26,31 @@ class AppExamsDatabase extends AppDatabase { 'Dezembro': '12' }; - static const _createScript = - '''CREATE TABLE exams(id TEXT, subject TEXT, begin TEXT, end TEXT, + static const _createScript = ''' +CREATE TABLE exams(id TEXT, subject TEXT, begin TEXT, end TEXT, rooms TEXT, examType TEXT, faculty TEXT, PRIMARY KEY (id,faculty)) '''; - AppExamsDatabase() - : super('exams.db', [_createScript], onUpgrade: migrate, version: 4); - /// Replaces all of the data in this database with [exams]. - saveNewExams(List exams) async { + Future saveNewExams(List exams) async { await deleteExams(); await _insertExams(exams); } /// Returns a list containing all of the exams stored in this database. Future> exams() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('exams'); return List.generate(maps.length, (i) { return Exam.secConstructor( - maps[i]['id'] ?? '', - maps[i]['subject'], - DateTime.parse(maps[i]['begin']), - DateTime.parse(maps[i]['end']), - maps[i]['rooms'], - maps[i]['examType'], - maps[i]['faculty']); + maps[i]['id'] as String, + maps[i]['subject'] as String, + DateTime.parse(maps[i]['begin'] as String), + DateTime.parse(maps[i]['end'] as String), + maps[i]['rooms'] as String, + maps[i]['examType'] as String, + maps[i]['faculty'] as String, + ); }); } @@ -57,7 +58,7 @@ class AppExamsDatabase extends AppDatabase { /// /// If a row with the same data is present, it will be replaced. Future _insertExams(List exams) async { - for (Exam exam in exams) { + for (final exam in exams) { await insertInDatabase( 'exams', exam.toMap(), @@ -69,7 +70,7 @@ class AppExamsDatabase extends AppDatabase { /// Deletes all of the data stored in this database. Future deleteExams() async { // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('exams'); } @@ -78,9 +79,9 @@ class AppExamsDatabase extends AppDatabase { int oldVersion, int newVersion, ) async { - final batch = db.batch(); - batch.execute('DROP TABLE IF EXISTS exams'); - batch.execute(_createScript); + final batch = db.batch() + ..execute('DROP TABLE IF EXISTS exams') + ..execute(_createScript); await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_last_user_info_update_database.dart b/uni/lib/controller/local_storage/app_last_user_info_update_database.dart index ed97ee8f2..93af19039 100644 --- a/uni/lib/controller/local_storage/app_last_user_info_update_database.dart +++ b/uni/lib/controller/local_storage/app_last_user_info_update_database.dart @@ -9,19 +9,19 @@ class AppLastUserInfoUpdateDatabase extends AppDatabase { : super('last_update.db', ['CREATE TABLE last_update(lastUpdate DATE)']); /// Replaces the timestamp in this database with [timestamp]. - insertNewTimeStamp(DateTime timestamp) async { + Future insertNewTimeStamp(DateTime timestamp) async { await deleteLastUpdate(); await _insertTimeStamp(timestamp); } /// Deletes all of the data from this database. - deleteLastUpdate() async { + Future deleteLastUpdate() async { final db = await getDatabase(); await db.delete('last_update'); } /// Replaces the timestamp of the last user info update with [timestamp]. - _insertTimeStamp(DateTime timestamp) async { + Future _insertTimeStamp(DateTime timestamp) async { final db = await getDatabase(); await db.transaction((txn) async { @@ -37,7 +37,7 @@ class AppLastUserInfoUpdateDatabase extends AppDatabase { final List> maps = await db.query('last_update'); if (maps.isNotEmpty) { - return DateTime.parse(maps[0]['lastUpdate']); + return DateTime.parse(maps[0]['lastUpdate'] as String); } return DateTime.now(); } diff --git a/uni/lib/controller/local_storage/app_lectures_database.dart b/uni/lib/controller/local_storage/app_lectures_database.dart index d4ec97b53..e1cce3121 100644 --- a/uni/lib/controller/local_storage/app_lectures_database.dart +++ b/uni/lib/controller/local_storage/app_lectures_database.dart @@ -1,47 +1,48 @@ import 'dart:async'; + +import 'package:sqflite/sqflite.dart'; import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/lecture.dart'; -import 'package:sqflite/sqflite.dart'; /// Manages the app's Lectures database. /// /// This database stores information about the user's lectures. /// See the [Lecture] class to see what data is stored in this database. class AppLecturesDatabase extends AppDatabase { - static const createScript = - '''CREATE TABLE lectures(subject TEXT, typeClass TEXT, - startDateTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; - AppLecturesDatabase() : super( - 'lectures.db', - [ - createScript, - ], - onUpgrade: migrate, - version: 6); + 'lectures.db', + [ + createScript, + ], + onUpgrade: migrate, + version: 6, + ); + static const createScript = ''' +CREATE TABLE lectures(subject TEXT, typeClass TEXT, + startDateTime TEXT, blocks INTEGER, room TEXT, teacher TEXT, classNumber TEXT, occurrId INTEGER)'''; - /// Replaces all of the data in this database with [lecs]. - saveNewLectures(List lecs) async { + /// Replaces all of the data in this database with [lectures]. + Future saveNewLectures(List lectures) async { await deleteLectures(); - await _insertLectures(lecs); + await _insertLectures(lectures); } /// Returns a list containing all of the lectures stored in this database. Future> lectures() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('lectures'); return List.generate(maps.length, (i) { return Lecture.fromApi( - maps[i]['subject'], - maps[i]['typeClass'], - maps[i]['startDateTime'], - maps[i]['blocks'], - maps[i]['room'], - maps[i]['teacher'], - maps[i]['classNumber'], - maps[i]['occurrId'], + maps[i]['subject'] as String, + maps[i]['typeClass'] as String, + maps[i]['startDateTime'] as DateTime, + maps[i]['blocks'] as int, + maps[i]['room'] as String, + maps[i]['teacher'] as String, + maps[i]['classNumber'] as String, + maps[i]['occurrId'] as int, ); }); } @@ -50,7 +51,7 @@ class AppLecturesDatabase extends AppDatabase { /// /// If a row with the same data is present, it will be replaced. Future _insertLectures(List lecs) async { - for (Lecture lec in lecs) { + for (final lec in lecs) { await insertInDatabase( 'lectures', lec.toMap(), @@ -62,7 +63,7 @@ class AppLecturesDatabase extends AppDatabase { /// Deletes all of the data stored in this database. Future deleteLectures() async { // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('lectures'); } @@ -72,10 +73,13 @@ class AppLecturesDatabase extends AppDatabase { /// *Note:* This operation only updates the schema of the tables present in /// the database and, as such, all data is lost. static FutureOr migrate( - Database db, int oldVersion, int newVersion) async { - final batch = db.batch(); - batch.execute('DROP TABLE IF EXISTS lectures'); - batch.execute(createScript); + Database db, + int oldVersion, + int newVersion, + ) async { + final batch = db.batch() + ..execute('DROP TABLE IF EXISTS lectures') + ..execute(createScript); await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_library_occupation_database.dart b/uni/lib/controller/local_storage/app_library_occupation_database.dart index 1f8c79fd8..a4ab4b35a 100644 --- a/uni/lib/controller/local_storage/app_library_occupation_database.dart +++ b/uni/lib/controller/local_storage/app_library_occupation_database.dart @@ -1,11 +1,11 @@ -import 'package:sqflite/sqflite.dart'; import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/library_occupation.dart'; class LibraryOccupationDatabase extends AppDatabase { LibraryOccupationDatabase() : super('occupation.db', [ - '''CREATE TABLE FLOOR_OCCUPATION( + ''' +CREATE TABLE FLOOR_OCCUPATION( id INTEGER PRIMARY KEY AUTOINCREMENT, number INT, occupation INT, @@ -14,26 +14,31 @@ class LibraryOccupationDatabase extends AppDatabase { ''' ]); - void saveOccupation(LibraryOccupation occupation) async { + Future saveOccupation(LibraryOccupation occupation) async { final db = await getDatabase(); - db.transaction((txn) async { + await db.transaction((txn) async { await txn.delete('FLOOR_OCCUPATION'); - for (var floor in occupation.floors) { + for (final floor in occupation.floors) { await txn.insert('FLOOR_OCCUPATION', floor.toMap()); } }); } Future occupation() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('floor_occupation'); - final LibraryOccupation occupation = LibraryOccupation(0, 0); + final occupation = LibraryOccupation(0, 0); - for (int i = 0; i < maps.length; i++) { - occupation.addFloor(FloorOccupation( - maps[i]['number'], maps[i]['occupation'], maps[i]['capacity'])); + for (var i = 0; i < maps.length; i++) { + occupation.addFloor( + FloorOccupation( + maps[i]['number'] as int, + maps[i]['occupation'] as int, + maps[i]['capacity'] as int, + ), + ); } return occupation; diff --git a/uni/lib/controller/local_storage/app_references_database.dart b/uni/lib/controller/local_storage/app_references_database.dart index daf8682bf..2693357a7 100644 --- a/uni/lib/controller/local_storage/app_references_database.dart +++ b/uni/lib/controller/local_storage/app_references_database.dart @@ -9,13 +9,12 @@ import 'package:uni/model/entities/reference.dart'; /// This database stores information about the user's references. /// See the [Reference] class to see what data is stored in this database. class AppReferencesDatabase extends AppDatabase { + AppReferencesDatabase() + : super('refs.db', [createScript], onUpgrade: migrate, version: 2); static const String createScript = '''CREATE TABLE refs(description TEXT, entity INTEGER, ''' '''reference INTEGER, amount REAL, limitDate TEXT)'''; - AppReferencesDatabase() - : super('refs.db', [createScript], onUpgrade: migrate, version: 2); - /// Replaces all of the data in this database with the data from [references]. Future saveNewReferences(List references) async { await deleteReferences(); @@ -24,22 +23,23 @@ class AppReferencesDatabase extends AppDatabase { /// Returns a list containing all the references stored in this database. Future> references() async { - final Database db = await getDatabase(); + final db = await getDatabase(); final List> maps = await db.query('refs'); return List.generate(maps.length, (i) { return Reference( - maps[i]['description'], - DateTime.parse(maps[i]['limitDate']), - maps[i]['entity'], - maps[i]['reference'], - maps[i]['amount']); + maps[i]['description'] as String, + DateTime.parse(maps[i]['limitDate'] as String), + maps[i]['entity'] as int, + maps[i]['reference'] as int, + maps[i]['amount'] as double, + ); }); } /// Deletes all of the data in this database. Future deleteReferences() async { - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('refs'); } @@ -47,21 +47,24 @@ class AppReferencesDatabase extends AppDatabase { /// /// If a row with the same data is present, it will be replaced. Future insertReferences(List references) async { - for (Reference reference in references) { - await insertInDatabase('refs', reference.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace); + for (final reference in references) { + await insertInDatabase( + 'refs', + reference.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); } } - /// Migrates [db] from [oldVersion] to [newVersion]. - /// - /// *Note:* This operation only updates the schema of the tables present in /// the database and, as such, all data is lost. static FutureOr migrate( - Database db, int oldVersion, int newVersion) async { - final batch = db.batch(); - batch.execute('DROP TABLE IF EXISTS refs'); - batch.execute(createScript); - batch.commit(); + Database db, + int oldVersion, + int newVersion, + ) async { + final batch = db.batch() + ..execute('DROP TABLE IF EXISTS refs') + ..execute(createScript); + await batch.commit(); } } diff --git a/uni/lib/controller/local_storage/app_refresh_times_database.dart b/uni/lib/controller/local_storage/app_refresh_times_database.dart deleted file mode 100644 index 52a327ec0..000000000 --- a/uni/lib/controller/local_storage/app_refresh_times_database.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'dart:async'; - -import 'package:sqflite/sqflite.dart'; -import 'package:uni/controller/local_storage/app_database.dart'; - -/// Manages the app's Refresh Times database. -/// -/// This database stores information about when certain data was updated -/// for the last time. -class AppRefreshTimesDatabase extends AppDatabase { - AppRefreshTimesDatabase() - : super('refreshtimes.db', - ['CREATE TABLE refreshtimes(event TEXT, time TEXT)']); - - /// Returns a map containing all the data stored in this database. - /// - /// *Note:* - /// * a key in this map is an event type. - /// * a value in this map is the timestamp at which the data of the given type - /// was last updated. - Future> refreshTimes() async { - final Database db = await getDatabase(); - final List> maps = await db.query('refreshtimes'); - - final Map refreshTimes = {}; - for (Map entry in maps) { - if (entry['event'] == 'print') refreshTimes['print'] = entry['time']; - if (entry['event'] == 'fees') refreshTimes['fees'] = entry['time']; - } - - return refreshTimes; - } - - /// Deletes all of the data from this database. - Future deleteRefreshTimes() async { - // Get a reference to the database - final Database db = await getDatabase(); - - await db.delete('refreshtimes'); - } - - /// Updates the time stored for an [event]. - void saveRefreshTime(String event, String time) async { - final Database db = await getDatabase(); - - final List maps = - await db.query('refreshtimes', where: 'event = ?', whereArgs: [event]); - - // New element - if (maps.isEmpty) { - await insertInDatabase('refreshtimes', {'event': event, 'time': time}); - } - // Update element - else { - await db.update( - 'refreshtimes', - {'time': time}, - where: 'event = ?', - whereArgs: [event], - ); - } - // store date ou julian days ou smt else - } -} diff --git a/uni/lib/controller/local_storage/app_restaurant_database.dart b/uni/lib/controller/local_storage/app_restaurant_database.dart index 4c0892bc3..56663b513 100644 --- a/uni/lib/controller/local_storage/app_restaurant_database.dart +++ b/uni/lib/controller/local_storage/app_restaurant_database.dart @@ -8,59 +8,72 @@ import 'package:uni/model/utils/day_of_week.dart'; class RestaurantDatabase extends AppDatabase { RestaurantDatabase() : super('restaurant.db', [ - 'CREATE TABLE RESTAURANTS(id INTEGER PRIMARY KEY, ref TEXT , name TEXT)', - '''CREATE TABLE MEALS( + ''' + CREATE TABLE RESTAURANTS( + id INTEGER PRIMARY KEY, + ref TEXT, + name TEXT) + ''', + ''' + CREATE TABLE MEALS( id INTEGER PRIMARY KEY AUTOINCREMENT, day TEXT, type TEXT, date TEXT, name TEXT, id_restaurant INTEGER, - FOREIGN KEY (id_restaurant) REFERENCES RESTAURANTS(id))''' + FOREIGN KEY (id_restaurant) REFERENCES RESTAURANTS(id)) + ''' ]); /// Deletes all data, and saves the new restaurants - void saveRestaurants(List restaurants) async { - final Database db = await getDatabase(); - db.transaction((transaction) async { + Future saveRestaurants(List restaurants) async { + final db = await getDatabase(); + await db.transaction((transaction) async { await deleteAll(transaction); - for (var restaurant in restaurants) { - insertRestaurant(transaction, restaurant); + for (final restaurant in restaurants) { + await insertRestaurant(transaction, restaurant); } }); } /// Get all restaurants and meals, if day is null, all meals are returned Future> restaurants({DayOfWeek? day}) async { - final Database db = await getDatabase(); - List restaurants = []; + final db = await getDatabase(); + var restaurants = []; await db.transaction((txn) async { final List> restaurantMaps = await db.query('restaurants'); - restaurants = await Future.wait(restaurantMaps.map((map) async { - final int restaurantId = map['id']; - final List meals = - await getRestaurantMeals(txn, restaurantId, day: day); - - return Restaurant(restaurantId, map['name'], map['ref'], meals: meals); - }).toList()); + restaurants = await Future.wait( + restaurantMaps.map((map) async { + final restaurantId = map['id'] as int; + final meals = await getRestaurantMeals(txn, restaurantId, day: day); + + return Restaurant( + restaurantId, + map['name'] as String, + map['ref'] as String, + meals: meals, + ); + }).toList(), + ); }); return restaurants; } Future> getRestaurants() async { - final Database db = await getDatabase(); - final List restaurants = []; + final db = await getDatabase(); + final restaurants = []; await db.transaction((txn) async { final List> restaurantsFromDB = await txn.query('RESTAURANTS'); - for (Map restaurantMap in restaurantsFromDB) { - final int id = restaurantMap['id']; - final List meals = await getRestaurantMeals(txn, id); - final Restaurant restaurant = Restaurant.fromMap(restaurantMap, meals); + for (final restaurantMap in restaurantsFromDB) { + final id = restaurantMap['id'] as int; + final meals = await getRestaurantMeals(txn, id); + final restaurant = Restaurant.fromMap(restaurantMap, meals); restaurants.add(restaurant); } }); @@ -68,10 +81,13 @@ class RestaurantDatabase extends AppDatabase { return filterPastMeals(restaurants); } - Future> getRestaurantMeals(Transaction txn, int restaurantId, - {DayOfWeek? day}) async { - final List whereArgs = [restaurantId]; - String whereQuery = 'id_restaurant = ? '; + Future> getRestaurantMeals( + Transaction txn, + int restaurantId, { + DayOfWeek? day, + }) async { + final whereArgs = [restaurantId]; + var whereQuery = 'id_restaurant = ? '; if (day != null) { whereQuery += ' and day = ?'; whereArgs.add(toString(day)); @@ -82,12 +98,12 @@ class RestaurantDatabase extends AppDatabase { await txn.query('meals', where: whereQuery, whereArgs: whereArgs); //Retrieve data from query - final List meals = mealsMaps.map((map) { - final DayOfWeek? day = parseDayOfWeek(map['day']); - final String type = map['type']; - final String name = map['name']; - final DateFormat format = DateFormat('d-M-y'); - final DateTime date = format.parseUtc(map['date']); + final meals = mealsMaps.map((map) { + final day = parseDayOfWeek(map['day'] as String); + final type = map['type'] as String; + final name = map['name'] as String; + final format = DateFormat('d-M-y'); + final date = format.parseUtc(map['date'] as String); return Meal(type, name, day!, date); }).toList(); @@ -96,9 +112,9 @@ class RestaurantDatabase extends AppDatabase { /// Insert restaurant and meals in database Future insertRestaurant(Transaction txn, Restaurant restaurant) async { - final int id = await txn.insert('RESTAURANTS', restaurant.toMap()); + final id = await txn.insert('RESTAURANTS', restaurant.toMap()); restaurant.meals.forEach((dayOfWeak, meals) async { - for (var meal in meals) { + for (final meal in meals) { await txn.insert('MEALS', meal.toMap(id)); } }); @@ -112,18 +128,18 @@ class RestaurantDatabase extends AppDatabase { } List filterPastMeals(List restaurants) { - final List restaurantsCopy = List.from(restaurants); + final restaurantsCopy = List.from(restaurants); // Hide past and next weeks' meals // (To replicate sigarra's behaviour for the GSheets meals) - final DateTime now = DateTime.now().toUtc(); - final DateTime today = DateTime.utc(now.year, now.month, now.day); - final DateTime nextSunday = - today.add(Duration(days: DateTime.sunday - now.weekday)); + final now = DateTime.now().toUtc(); + final today = DateTime.utc(now.year, now.month, now.day); + final nextSunday = today.add(Duration(days: DateTime.sunday - now.weekday)); - for (var restaurant in restaurantsCopy) { - for (var meals in restaurant.meals.values) { + for (final restaurant in restaurantsCopy) { + for (final meals in restaurant.meals.values) { meals.removeWhere( - (meal) => meal.date.isBefore(today) || meal.date.isAfter(nextSunday)); + (meal) => meal.date.isBefore(today) || meal.date.isAfter(nextSunday), + ); } } diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/app_shared_preferences.dart index 6db64b9ed..bd11d8aa3 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/app_shared_preferences.dart @@ -12,14 +12,14 @@ import 'package:uni/utils/favorite_widget_type.dart'; /// This database stores the user's student number, password and favorite /// widgets. class AppSharedPreferences { - static const lastUpdateTimeKeySuffix = "_last_update_time"; + static const 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"; + 'tuition_notification_toogle'; static const String themeMode = 'theme_mode'; static const int keyLength = 32; static const int ivLength = 16; @@ -44,24 +44,37 @@ class AppSharedPreferences { /// Sets the last time the data with given key was updated. static Future setLastDataClassUpdateTime( - String dataKey, DateTime dateTime) async { + String dataKey, + DateTime dateTime, + ) async { final prefs = await SharedPreferences.getInstance(); - prefs.setString(dataKey + lastUpdateTimeKeySuffix, dateTime.toString()); + await prefs.setString( + dataKey + lastUpdateTimeKeySuffix, + dateTime.toString(), + ); } /// Saves the user's student number, password and faculties. - static Future savePersistentUserInfo(user, pass, faculties) async { + static Future savePersistentUserInfo( + String user, + String pass, + List faculties, + ) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(userNumber, user); await prefs.setString(userPw, encode(pass)); await prefs.setStringList( - userFaculties, faculties); // Could be multiple faculties + userFaculties, + faculties, + ); // Could be multiple faculties } /// Sets whether or not the Terms and Conditions have been accepted. - static Future setTermsAndConditionsAcceptance(bool areAccepted) async { + static Future setTermsAndConditionsAcceptance({ + required bool areAccepted, + }) async { final prefs = await SharedPreferences.getInstance(); - prefs.setBool(areTermsAndConditionsAcceptedKey, areAccepted); + await prefs.setBool(areTermsAndConditionsAcceptedKey, areAccepted); } /// Returns whether or not the Terms and Conditions have been accepted. @@ -104,10 +117,10 @@ class AppSharedPreferences { } /// Deletes the user's student number and password. - static Future removePersistentUserInfo() async { + static Future removePersistentUserInfo() async { final prefs = await SharedPreferences.getInstance(); - prefs.remove(userNumber); - prefs.remove(userPw); + await prefs.remove(userNumber); + await prefs.remove(userPw); } /// Returns a tuple containing the user's student number and password. @@ -117,17 +130,17 @@ class AppSharedPreferences { /// * the second element in the tuple is the user's password, in plain text /// format. static Future> getPersistentUserInfo() async { - final String userNum = await getUserNumber(); - final String userPass = await getUserPassword(); + final userNum = await getUserNumber(); + final userPass = await getUserPassword(); return Tuple2(userNum, userPass); } /// Returns the user's faculties static Future> getUserFaculties() async { final prefs = await SharedPreferences.getInstance(); - final List? storedFaculties = prefs.getStringList(userFaculties); - return storedFaculties ?? - ['feup']; // TODO: Store dropdown choices in the db for later storage; + 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. @@ -140,7 +153,7 @@ class AppSharedPreferences { /// Returns the user's password, in plain text format. static Future getUserPassword() async { final prefs = await SharedPreferences.getInstance(); - String pass = prefs.getString(userPw) ?? ''; + var pass = prefs.getString(userPw) ?? ''; if (pass != '') { pass = decode(pass); @@ -150,55 +163,61 @@ class AppSharedPreferences { } /// Replaces the user's favorite widgets with [newFavorites]. - static saveFavoriteCards(List newFavorites) async { + static Future saveFavoriteCards( + List newFavorites, + ) async { final prefs = await SharedPreferences.getInstance(); - prefs.setStringList( - favoriteCards, newFavorites.map((a) => a.index.toString()).toList()); + await prefs.setStringList( + 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(); - final List? storedFavorites = prefs.getStringList(favoriteCards); + final storedFavorites = prefs.getStringList(favoriteCards); if (storedFavorites == null) return defaultFavoriteCards; return storedFavorites .map((i) => FavoriteWidgetType.values[int.parse(i)]) .toList(); } - static saveHiddenExams(List newHiddenExams) async { + static Future saveHiddenExams(List newHiddenExams) async { final prefs = await SharedPreferences.getInstance(); - prefs.setStringList(hiddenExams, newHiddenExams); + await prefs.setStringList(hiddenExams, newHiddenExams); } static Future> getHiddenExams() async { final prefs = await SharedPreferences.getInstance(); - final List storedHiddenExam = - prefs.getStringList(hiddenExams) ?? []; + final storedHiddenExam = prefs.getStringList(hiddenExams) ?? []; return storedHiddenExam; } /// Replaces the user's exam filter settings with [newFilteredExamTypes]. - static saveFilteredExams(Map newFilteredExamTypes) async { + static Future saveFilteredExams( + Map newFilteredExamTypes, + ) async { final prefs = await SharedPreferences.getInstance(); - final List newTypes = newFilteredExamTypes.keys - .where((type) => newFilteredExamTypes[type] == true) + final newTypes = newFilteredExamTypes.keys + .where((type) => newFilteredExamTypes[type] ?? false) .toList(); - 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 List? storedFilteredExamTypes = - prefs.getStringList(filteredExamsTypes); + final storedFilteredExamTypes = prefs.getStringList(filteredExamsTypes); if (storedFilteredExamTypes == null) { return Map.fromIterable(defaultFilteredExamTypes, value: (type) => true); } - return Map.fromIterable(defaultFilteredExamTypes, - value: (type) => storedFilteredExamTypes.contains(type)); + return Map.fromIterable( + defaultFilteredExamTypes, + value: storedFilteredExamTypes.contains, + ); } /// Encrypts [plainText] and returns its base64 representation. @@ -213,7 +232,8 @@ class AppSharedPreferences { return encrypter.decrypt64(base64Text, iv: iv); } - /// Creates an [Encrypter] for encrypting and decrypting the user's password. + /// Creates an [encrypt.Encrypter] for encrypting and decrypting the user's + /// password. static encrypt.Encrypter _createEncrypter() { final key = encrypt.Key.fromLength(keyLength); return encrypt.Encrypter(encrypt.AES(key)); @@ -224,8 +244,10 @@ class AppSharedPreferences { return prefs.getBool(tuitionNotificationsToggleKey) ?? true; } - static setTuitionNotificationToggle(bool value) async { + static Future setTuitionNotificationToggle({ + required bool value, + }) async { final prefs = await SharedPreferences.getInstance(); - prefs.setBool(tuitionNotificationsToggleKey, value); + await prefs.setBool(tuitionNotificationsToggleKey, value); } } diff --git a/uni/lib/controller/local_storage/app_user_database.dart b/uni/lib/controller/local_storage/app_user_database.dart index b734359ca..4a2eca630 100644 --- a/uni/lib/controller/local_storage/app_user_database.dart +++ b/uni/lib/controller/local_storage/app_user_database.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:sqflite/sqflite.dart'; -import 'package:tuple/tuple.dart'; import 'package:uni/controller/local_storage/app_database.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; @@ -15,62 +13,73 @@ class AppUserDataDatabase extends AppDatabase { /// Adds [profile] to this database. Future insertUserData(Profile profile) async { - // TODO: Change profile keymap logic to avoid conflicts with print balance (#526) - for (Tuple2 keymap in profile.keymapValues()) { + for (final keymap in profile.keymapValues()) { await insertInDatabase( - 'userdata', {'key': keymap.item1, 'value': keymap.item2}); + 'userdata', + {'key': keymap.item1, 'value': keymap.item2}, + ); } } // Returns all of the data stored in this database. Future getUserData() async { // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); // Query the table for all the user data final List> maps = await db.query('userdata'); // Convert the List into a Profile. - String? name, email, printBalance, feesBalance; + String? name; + String? email; + String? printBalance; + String? feesBalance; DateTime? feesLimit; - for (Map entry in maps) { - if (entry['key'] == 'name') name = entry['value']; - if (entry['key'] == 'email') email = entry['value']; - if (entry['key'] == 'printBalance') printBalance = entry['value']; - if (entry['key'] == 'feesBalance') feesBalance = entry['value']; + for (final entry in maps) { + if (entry['key'] == 'name') name = entry['value'] as String; + if (entry['key'] == 'email') email = entry['value'] as String; + if (entry['key'] == 'printBalance') { + printBalance = entry['value'] as String; + } + if (entry['key'] == 'feesBalance') feesBalance = entry['value'] as String; if (entry['key'] == 'feesLimit') { - feesLimit = DateTime.tryParse(entry['value']); + feesLimit = DateTime.tryParse(entry['value'] as String); } } return Profile( - name: name ?? '?', - email: email ?? '?', - courses: [], - printBalance: printBalance ?? '?', - feesBalance: feesBalance ?? '?', - feesLimit: feesLimit); + name: name ?? '?', + email: email ?? '?', + courses: [], + printBalance: printBalance ?? '?', + feesBalance: feesBalance ?? '?', + feesLimit: feesLimit, + ); } /// Deletes all of the data stored in this database. Future deleteUserData() async { // Get a reference to the database - final Database db = await getDatabase(); + final db = await getDatabase(); await db.delete('userdata'); } /// Saves the user's print balance to the database. - void saveUserPrintBalance(String userBalance) async { + Future saveUserPrintBalance(String userBalance) async { await insertInDatabase( - 'userdata', {'key': 'printBalance', 'value': userBalance}); + 'userdata', + {'key': 'printBalance', 'value': userBalance}, + ); } /// Saves the user's balance and payment due date to the database. /// - void saveUserFees(String feesBalance, DateTime? feesLimit) async { + Future saveUserFees(String feesBalance, DateTime? feesLimit) async { await insertInDatabase( - 'userdata', {'key': 'feesBalance', 'value': feesBalance}); + 'userdata', + {'key': 'feesBalance', 'value': feesBalance}, + ); await insertInDatabase('userdata', { 'key': 'feesLimit', 'value': feesLimit != null ? feesLimit.toIso8601String() : '' diff --git a/uni/lib/controller/local_storage/file_offline_storage.dart b/uni/lib/controller/local_storage/file_offline_storage.dart index cb24fafc6..1ae925910 100644 --- a/uni/lib/controller/local_storage/file_offline_storage.dart +++ b/uni/lib/controller/local_storage/file_offline_storage.dart @@ -15,14 +15,18 @@ Future get _localPath async { /// Gets cached image named [localFileName]. /// If not found or too old, downloads it from [url] with [headers]. Future loadFileFromStorageOrRetrieveNew( - String localFileName, String url, Map headers, - {int staleDays = 7, forceRetrieval = false}) async { + String localFileName, + String url, + Map headers, { + int staleDays = 7, + bool forceRetrieval = false, +}) async { final path = await _localPath; final targetPath = '$path/$localFileName'; - final File file = File(targetPath); + final file = File(targetPath); - final bool fileExists = file.existsSync(); - final bool fileIsStale = forceRetrieval || + final fileExists = file.existsSync(); + final fileIsStale = forceRetrieval || (fileExists && file .lastModifiedSync() @@ -32,8 +36,7 @@ Future loadFileFromStorageOrRetrieveNew( return file; } if (await Connectivity().checkConnectivity() != ConnectivityResult.none) { - final File? downloadedFile = - await _downloadAndSaveFile(targetPath, url, headers); + final downloadedFile = await _downloadAndSaveFile(targetPath, url, headers); if (downloadedFile != null) { return downloadedFile; } @@ -43,7 +46,10 @@ Future loadFileFromStorageOrRetrieveNew( /// Downloads the image located at [url] and saves it in [filePath]. Future _downloadAndSaveFile( - String filePath, String url, Map headers) async { + String filePath, + String url, + Map headers, +) async { final response = await http.get(url.toUri(), headers: headers); if (response.statusCode == 200) { return File(filePath).writeAsBytes(response.bodyBytes); diff --git a/uni/lib/controller/local_storage/notification_timeout_storage.dart b/uni/lib/controller/local_storage/notification_timeout_storage.dart index 6875a5293..b74800271 100644 --- a/uni/lib/controller/local_storage/notification_timeout_storage.dart +++ b/uni/lib/controller/local_storage/notification_timeout_storage.dart @@ -1,12 +1,13 @@ import 'dart:convert'; import 'dart:io'; + import 'package:path_provider/path_provider.dart'; class NotificationTimeoutStorage { - late Map _fileContent; - NotificationTimeoutStorage._create(); + late Map _fileContent; + Future _asyncInit() async { _fileContent = _readContentsFile(await _getTimeoutFile()); } @@ -19,7 +20,7 @@ class NotificationTimeoutStorage { Map _readContentsFile(File file) { try { - return jsonDecode(file.readAsStringSync()); + return jsonDecode(file.readAsStringSync()) as Map; } on FormatException catch (_) { return {}; } @@ -28,13 +29,16 @@ class NotificationTimeoutStorage { DateTime getLastTimeNotificationExecuted(String uniqueID) { if (!_fileContent.containsKey(uniqueID)) { return DateTime.fromMicrosecondsSinceEpoch( - 0); //get 1970 to always trigger notification + 0, + ); //get 1970 to always trigger notification } - return DateTime.parse(_fileContent[uniqueID]); + return DateTime.parse(_fileContent[uniqueID] as String); } Future addLastTimeNotificationExecuted( - String uniqueID, DateTime lastRan) async { + String uniqueID, + DateTime lastRan, + ) async { _fileContent[uniqueID] = lastRan.toIso8601String(); await _writeToFile(await _getTimeoutFile()); } @@ -46,12 +50,11 @@ class NotificationTimeoutStorage { Future _getTimeoutFile() async { final applicationDirectory = (await getApplicationDocumentsDirectory()).path; - if (!(await File("$applicationDirectory/notificationTimeout.json") - .exists())) { + if (!File('$applicationDirectory/notificationTimeout.json').existsSync()) { //empty json - await File("$applicationDirectory/notificationTimeout.json") - .writeAsString("{}"); + await File('$applicationDirectory/notificationTimeout.json') + .writeAsString('{}'); } - return File("$applicationDirectory/notificationTimeout.json"); + return File('$applicationDirectory/notificationTimeout.json'); } } diff --git a/uni/lib/controller/logout.dart b/uni/lib/controller/logout.dart index 3f7c6b473..a25735549 100644 --- a/uni/lib/controller/logout.dart +++ b/uni/lib/controller/logout.dart @@ -10,26 +10,31 @@ 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_refresh_times_database.dart'; +import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/app_user_database.dart'; -import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/controller/local_storage/app_shared_preferences.dart'; +import 'package:uni/model/providers/state_providers.dart'; +import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; + +Future logout(BuildContext context) async { + StateProviders.fromContext(context).markAsNotInitialized(); -Future logout(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); final faculties = await AppSharedPreferences.getUserFaculties(); await prefs.clear(); - AppLecturesDatabase().deleteLectures(); - AppExamsDatabase().deleteExams(); - AppCoursesDatabase().deleteCourses(); - AppRefreshTimesDatabase().deleteRefreshTimes(); - AppUserDataDatabase().deleteUserData(); - AppLastUserInfoUpdateDatabase().deleteLastUpdate(); - AppBusStopDatabase().deleteBusStops(); - AppCourseUnitsDatabase().deleteCourseUnits(); - NetworkRouter.killSigarraAuthentication(faculties); + unawaited( + Future.wait([ + AppLecturesDatabase().deleteLectures(), + AppExamsDatabase().deleteExams(), + AppCoursesDatabase().deleteCourses(), + AppUserDataDatabase().deleteUserData(), + AppLastUserInfoUpdateDatabase().deleteLastUpdate(), + AppBusStopDatabase().deleteBusStops(), + AppCourseUnitsDatabase().deleteCourseUnits(), + NetworkRouter.killSigarraAuthentication(faculties), + ]), + ); final path = (await getApplicationDocumentsDirectory()).path; final directory = Directory(path); diff --git a/uni/lib/controller/networking/network_router.dart b/uni/lib/controller/networking/network_router.dart index 578b391df..b2fd02ba8 100644 --- a/uni/lib/controller/networking/network_router.dart +++ b/uni/lib/controller/networking/network_router.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; 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'; @@ -29,24 +30,31 @@ class NetworkRouter { /// Performs a login using the Sigarra API, /// returning an authenticated [Session] on the given [faculties] with the /// given username [username] and password [password] if successful. - static Future login(String username, String password, - List faculties, bool persistentSession) async { + static Future login( + String username, + String password, + List faculties, { + required bool persistentSession, + }) async { return _loginLock.synchronized(() async { - final String url = + final url = '${NetworkRouter.getBaseUrls(faculties)[0]}mob_val_geral.autentica'; - final http.Response response = await http.post(url.toUri(), body: { - 'pv_login': username, - 'pv_password': password - }).timeout(_requestTimeout); + final response = await http.post( + url.toUri(), + body: {'pv_login': username, 'pv_password': password}, + ).timeout(_requestTimeout); if (response.statusCode != 200) { - Logger().e("Login failed with status code ${response.statusCode}"); + Logger().e('Login failed with status code ${response.statusCode}'); return null; } - final Session? session = - Session.fromLogin(response, faculties, persistentSession); + final session = Session.fromLogin( + response, + faculties, + persistentSession: persistentSession, + ); if (session == null) { Logger().e('Login failed: user not authenticated'); return null; @@ -61,26 +69,36 @@ class NetworkRouter { /// using data stored in [session], /// returning an updated Session if successful. static Future reLoginFromSession(Session session) async { - final String username = session.username; - final String password = await AppSharedPreferences.getUserPassword(); - final List faculties = session.faculties; - final bool persistentSession = session.persistentSession; + final username = session.username; + final password = await AppSharedPreferences.getUserPassword(); + final faculties = session.faculties; + final persistentSession = session.persistentSession; Logger().i('Re-logging in user $username'); - return await login(username, password, faculties, persistentSession); + return login( + username, + password, + faculties, + persistentSession: persistentSession, + ); } /// Returns the response body of the login in Sigarra /// given username [user] and password [pass]. static Future loginInSigarra( - String user, String pass, List faculties) async { + String user, + String pass, + List faculties, + ) async { return _loginLock.synchronized(() async { - final String url = + final url = '${NetworkRouter.getBaseUrls(faculties)[0]}vld_validacao.validacao'; - final response = await http.post(url.toUri(), - body: {'p_user': user, 'p_pass': pass}).timeout(_requestTimeout); + final response = await http.post( + url.toUri(), + body: {'p_user': user, 'p_pass': pass}, + ).timeout(_requestTimeout); return response.body; }); @@ -88,12 +106,12 @@ class NetworkRouter { /// Extracts the cookies present in [headers]. static String extractCookies(Map headers) { - final List cookieList = []; - final String? cookies = headers['set-cookie']; + final cookieList = []; + final cookies = headers['set-cookie']; if (cookies != null && cookies != '') { - final List rawCookies = cookies.split(','); - for (var c in rawCookies) { + final rawCookies = cookies.split(','); + for (final c in rawCookies) { cookieList.add(Cookie.fromSetCookieValue(c).toString()); } } @@ -102,16 +120,19 @@ class NetworkRouter { } /// Makes an authenticated GET request with the given [session] to the - /// resource located at [url] with the given [query] parameters. + /// resource located at [baseUrl] with the given [query] parameters. /// If the request fails with a 403 status code, the user is re-authenticated /// and the session is updated. static Future getWithCookies( - String baseUrl, Map query, Session session, - {Duration timeout = _requestTimeout}) async { - if (!baseUrl.contains('?')) { - baseUrl += '?'; + String baseUrl, + Map query, + Session session, { + Duration timeout = _requestTimeout, + }) async { + var url = baseUrl; + if (!url.contains('?')) { + url += '?'; } - String url = baseUrl; query.forEach((key, value) { url += '$key=$value&'; }); @@ -119,10 +140,10 @@ class NetworkRouter { url = url.substring(0, url.length - 1); } - final Map headers = {}; + final headers = {}; headers['cookie'] = session.cookies; - final http.Response response = await (httpClient != null + final response = await (httpClient != null ? httpClient!.get(url.toUri(), headers: headers).timeout(timeout) : http.get(url.toUri(), headers: headers)) .timeout(timeout); @@ -135,7 +156,7 @@ class NetworkRouter { if (forbidden) { final userIsLoggedIn = await userLoggedIn(session); if (!userIsLoggedIn) { - final Session? newSession = await reLoginFromSession(session); + final newSession = await reLoginFromSession(session); if (newSession == null) { NavigationService.logout(); @@ -147,8 +168,10 @@ class NetworkRouter { headers['cookie'] = session.cookies; return http.get(url.toUri(), headers: headers).timeout(timeout); } else { - // If the user is logged in but still got a 403, they are forbidden to access the resource - // or the login was invalid at the time of the request, but other thread re-authenticated. + // If the user is logged in but still got a 403, they are + // forbidden to access the resource or the login was invalid + // at the time of the request, + // but other thread re-authenticated. // Since we do not know which one is the case, we try again. headers['cookie'] = session.cookies; final response = @@ -166,11 +189,11 @@ class NetworkRouter { /// performing a health check on the user's personal page. static Future userLoggedIn(Session session) async { return _loginLock.synchronized(() async { - final url = - '${getBaseUrl(session.faculties[0])}fest_geral.cursos_list?pv_num_unico=${session.username}'; - final Map headers = {}; + final url = '${getBaseUrl(session.faculties[0])}' + 'fest_geral.cursos_list?pv_num_unico=${session.username}'; + final headers = {}; headers['cookie'] = session.cookies; - final http.Response response = await (httpClient != null + final response = await (httpClient != null ? httpClient!.get(url.toUri(), headers: headers) : http.get(url.toUri(), headers: headers)); return response.statusCode == 200; @@ -193,15 +216,17 @@ class NetworkRouter { } /// Makes an HTTP request to terminate the session in Sigarra. - static Future killSigarraAuthentication(List faculties) async { + static Future killSigarraAuthentication( + List faculties, + ) async { return _loginLock.synchronized(() async { final url = '${NetworkRouter.getBaseUrl(faculties[0])}vld_validacao.sair'; final response = await http.get(url.toUri()).timeout(_requestTimeout); if (response.statusCode == 200) { - Logger().i("Logout Successful"); + Logger().i('Logout Successful'); } else { - Logger().i("Logout Failed"); + Logger().i('Logout Failed'); } return response; diff --git a/uni/lib/controller/parsers/parser_calendar.dart b/uni/lib/controller/parsers/parser_calendar.dart index 497da6c41..77af03e39 100644 --- a/uni/lib/controller/parsers/parser_calendar.dart +++ b/uni/lib/controller/parsers/parser_calendar.dart @@ -1,16 +1,18 @@ -import 'package:html/dom.dart'; -import 'package:http/http.dart'; import 'package:html/parser.dart'; - +import 'package:http/http.dart'; import 'package:uni/model/entities/calendar_event.dart'; Future> getCalendarFromHtml(Response response) async { final document = parse(response.body); - final List calendarHtml = document.querySelectorAll('tr'); + final calendarHtml = document.querySelectorAll('tr'); return calendarHtml - .map((event) => CalendarEvent( - event.children[0].innerHtml, event.children[1].innerHtml)) + .map( + (event) => CalendarEvent( + event.children[0].innerHtml, + event.children[1].innerHtml, + ), + ) .toList(); } diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 483242d1b..6ef395a2f 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -6,11 +6,16 @@ import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; Future parseCourseUnitSheet(http.Response response) async { final document = parse(response.body); final titles = document.querySelectorAll('#conteudoinner h3'); - final Map sections = {}; + final sections = {}; + + for (final title in titles) { + if (title.text.trim().isEmpty) { + continue; + } - for (var title in titles) { try { - sections[title.text] = _htmlAfterElement(response.body, title.outerHtml); + sections[title.text.trim()] = + _htmlAfterElement(response.body, title.outerHtml); } catch (_) { continue; } @@ -20,15 +25,19 @@ Future parseCourseUnitSheet(http.Response response) async { } List parseCourseUnitClasses( - http.Response response, String baseUrl) { - final List classes = []; + http.Response response, + String baseUrl, +) { + final classes = []; final document = parse(response.body); final titles = document.querySelectorAll('#conteudoinner h3').sublist(1); for (final title in titles) { final table = title.nextElementSibling; - final String className = title.innerHtml.substring( - title.innerHtml.indexOf(' ') + 1, title.innerHtml.indexOf('&')); + final className = title.innerHtml.substring( + title.innerHtml.indexOf(' ') + 1, + title.innerHtml.indexOf('&'), + ); final rows = table?.querySelectorAll('tr'); if (rows == null || rows.length < 2) { @@ -36,20 +45,29 @@ List parseCourseUnitClasses( } final studentRows = rows.sublist(1); - final List students = []; + final students = []; for (final row in studentRows) { final columns = row.querySelectorAll('td.k.t'); - final String studentName = columns[0].children[0].innerHtml; - final int studentNumber = int.tryParse(columns[1].innerHtml.trim()) ?? 0; - final String studentMail = columns[2].innerHtml; + final studentName = columns[0].children[0].innerHtml; + final studentNumber = int.tryParse(columns[1].innerHtml.trim()) ?? 0; + final studentMail = columns[2].innerHtml; - final Uri studentPhoto = Uri.parse( - "${baseUrl}fotografias_service.foto?pct_cod=$studentNumber"); - final Uri studentProfile = Uri.parse( - "${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber"); - students.add(CourseUnitStudent(studentName, studentNumber, studentMail, - studentPhoto, studentProfile)); + final studentPhoto = Uri.parse( + '${baseUrl}fotografias_service.foto?pct_cod=$studentNumber', + ); + final studentProfile = Uri.parse( + '${baseUrl}fest_geral.cursos_list?pv_num_unico=$studentNumber', + ); + students.add( + CourseUnitStudent( + studentName, + studentNumber, + studentMail, + studentPhoto, + studentProfile, + ), + ); } classes.add(CourseUnitClass(className, students)); @@ -59,6 +77,6 @@ List parseCourseUnitClasses( } String _htmlAfterElement(String body, String elementOuterHtml) { - final int index = body.indexOf(elementOuterHtml) + elementOuterHtml.length; + final index = body.indexOf(elementOuterHtml) + elementOuterHtml.length; return body.substring(index, body.indexOf('

', index)); } diff --git a/uni/lib/controller/parsers/parser_course_units.dart b/uni/lib/controller/parsers/parser_course_units.dart index 1414226d1..179ba7b77 100644 --- a/uni/lib/controller/parsers/parser_course_units.dart +++ b/uni/lib/controller/parsers/parser_course_units.dart @@ -5,7 +5,9 @@ import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/utils/url_parser.dart'; List parseCourseUnitsAndCourseAverage( - http.Response response, Course course) { + http.Response response, + Course course, +) { final document = parse(response.body); final table = document.getElementById('tabelapercurso'); if (table == null) { @@ -15,22 +17,25 @@ List parseCourseUnitsAndCourseAverage( final labels = document.querySelectorAll('.caixa .formulario-legenda'); if (labels.length >= 2) { course.currentAverage ??= num.tryParse( - labels[0].nextElementSibling?.innerHtml.replaceFirst(',', '.') ?? '0'); + labels[0].nextElementSibling?.innerHtml.replaceFirst(',', '.') ?? '0', + ); course.finishedEcts ??= num.tryParse( - labels[1].nextElementSibling?.innerHtml.replaceFirst(',', '.') ?? '0'); + labels[1].nextElementSibling?.innerHtml.replaceFirst(',', '.') ?? '0', + ); } - final String? firstSchoolYearData = + final firstSchoolYearData = table.querySelector('tr')?.children[1].text.trim(); if (firstSchoolYearData == null) { return []; } - final int firstSchoolYear = int.parse( - firstSchoolYearData.substring(0, firstSchoolYearData.indexOf('/'))); + final firstSchoolYear = int.parse( + firstSchoolYearData.substring(0, firstSchoolYearData.indexOf('/')), + ); // Each row contains: // ANO PERIODO CODIGO NOME OPCAO/MINOR CREDITOS RESULTADO ESTADO - final List courseUnits = []; + final courseUnits = []; final rows = table.querySelectorAll('tr.i, tr.p'); for (final row in rows) { @@ -39,16 +44,17 @@ List parseCourseUnitsAndCourseAverage( continue; } - final String year = row.children[0].innerHtml; - final String semester = row.children[1].innerHtml; - final String? occurId = getUrlQueryParameters( - row.children[2].firstChild?.attributes['href'] ?? - '')['pv_ocorrencia_id']; - final String codeName = row.children[2].children[0].innerHtml; - final String name = row.children[3].children[0].innerHtml; - final String ects = row.children[5].innerHtml.replaceAll(',', '.'); - String? grade, status; - int yearIncrement = -1; + final year = row.children[0].innerHtml; + final semester = row.children[1].innerHtml; + final occurId = getUrlQueryParameters( + row.children[2].firstChild!.attributes['href']!, + )['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(',', '.'); + String? grade; + String? status; + var yearIncrement = -1; for (var i = 0;; i += 2) { if (row.children.length <= 6 + i) { break; @@ -64,17 +70,18 @@ List parseCourseUnitsAndCourseAverage( continue; } - final CourseUnit courseUnit = CourseUnit( - schoolYear: - '${firstSchoolYear + yearIncrement}/${firstSchoolYear + yearIncrement + 1}', - occurrId: occurId != null ? int.parse(occurId) : 0, - abbreviation: codeName, - status: status, - grade: grade, - ects: double.parse(ects), - name: name, - curricularYear: int.parse(year), - semesterCode: semester); + final courseUnit = CourseUnit( + schoolYear: + '${firstSchoolYear + yearIncrement}/${firstSchoolYear + yearIncrement + 1}', + occurrId: int.parse(occurId), + abbreviation: codeName, + status: status, + grade: grade, + ects: double.parse(ects), + name: name, + curricularYear: int.parse(year), + semesterCode: semester, + ); courseUnits.add(courseUnit); } diff --git a/uni/lib/controller/parsers/parser_courses.dart b/uni/lib/controller/parsers/parser_courses.dart index cc7a1d93a..10b1e6d97 100644 --- a/uni/lib/controller/parsers/parser_courses.dart +++ b/uni/lib/controller/parsers/parser_courses.dart @@ -4,8 +4,8 @@ import 'package:uni/model/entities/course.dart'; import 'package:uni/utils/url_parser.dart'; List parseMultipleCourses(List responses) { - final List courses = []; - for (var response in responses) { + final courses = []; + for (final response in responses) { courses.addAll(_parseCourses(response)); } return courses; @@ -13,15 +13,14 @@ List parseMultipleCourses(List responses) { List _parseCourses(http.Response response) { final document = parse(response.body); - final List courses = []; + final courses = []; - final String stringUrl = response.request?.url.toString() ?? ''; - final String? faculty = - stringUrl.contains('up.pt') ? stringUrl.split('/')[3] : null; + final stringUrl = response.request?.url.toString() ?? ''; + final faculty = stringUrl.contains('up.pt') ? stringUrl.split('/')[3] : null; final currentCourses = document.querySelectorAll('.estudantes-caixa-lista-cursos > div'); - for (int i = 0; i < currentCourses.length; i++) { + for (var i = 0; i < currentCourses.length; i++) { final div = currentCourses[i]; final courseName = div.querySelector('.estudante-lista-curso-nome > a')?.text; @@ -34,19 +33,24 @@ List _parseCourses(http.Response response) { .querySelector('.estudante-lista-curso-detalhes > a') ?.attributes['href'] ?.replaceFirst( - 'fest_geral.curso_percurso_academico_view?pv_fest_id=', '') + 'fest_geral.curso_percurso_academico_view?pv_fest_id=', + '', + ) .trim(); - courses.add(Course( + courses.add( + Course( faculty: faculty, id: int.parse(courseId ?? '0'), state: courseState, name: courseName ?? '', - festId: int.parse(courseFestId ?? '0'))); + festId: int.parse(courseFestId ?? '0'), + ), + ); } final oldCourses = document.querySelectorAll('.tabela-longa .i, .tabela-longa .p'); - for (int i = 0; i < oldCourses.length; i++) { + for (var i = 0; i < oldCourses.length; i++) { final div = oldCourses[i]; final courseName = div.children[0].firstChild?.text?.trim(); final courseUrl = div.querySelector('a')?.attributes['href']; @@ -57,14 +61,18 @@ List _parseCourses(http.Response response) { .trim(); final courseState = div.children[5].text; final courseFestId = getUrlQueryParameters( - div.children[6].firstChild?.attributes['href'] ?? '')['pv_fest_id']; - courses.add(Course( + div.children[6].firstChild?.attributes['href'] ?? '', + )['pv_fest_id']; + courses.add( + Course( firstEnrollment: int.parse(courseFirstEnrollment), faculty: faculty, id: int.parse(courseId ?? '0'), state: courseState, name: courseName ?? '', - festId: int.parse(courseFestId ?? '0'))); + festId: int.parse(courseFestId ?? '0'), + ), + ); } return courses; diff --git a/uni/lib/controller/parsers/parser_exams.dart b/uni/lib/controller/parsers/parser_exams.dart index aa7661f5e..6c4cd8875 100644 --- a/uni/lib/controller/parsers/parser_exams.dart +++ b/uni/lib/controller/parsers/parser_exams.dart @@ -12,7 +12,7 @@ class ParserExams { /// /// If an abbreviature doesn't exist, a '?' is returned. String getExamSeasonAbbr(String seasonStr) { - for (String type in Exam.types.keys) { + for (final type in Exam.types.keys) { if (seasonStr.contains(type)) return Exam.types[type]!; } return '?'; @@ -22,14 +22,15 @@ class ParserExams { Future> parseExams(http.Response response, Course course) async { final document = parse(response.body); - final Set examsList = {}; - final List dates = []; - final List examTypes = []; - List rooms = []; - String? subject, schedule; - String id = '0'; - int days = 0; - int tableNum = 0; + final examsList = {}; + final dates = []; + final examTypes = []; + var rooms = []; + String? subject; + String? schedule; + var id = '0'; + var days = 0; + var tableNum = 0; document.querySelectorAll('h3').forEach((Element examType) { examTypes.add(getExamSeasonAbbr(examType.text)); }); @@ -53,15 +54,24 @@ class ParserExams { rooms = examsDay.querySelector('span.exame-sala')!.text.split(','); } - schedule = examsDay.text.substring(examsDay.text.indexOf(':') - 2, - examsDay.text.indexOf(':') + 9); - final List splittedSchedule = schedule!.split('-'); - final DateTime begin = + 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 DateTime end = + final end = DateTime.parse('${dates[days]} ${splittedSchedule[1]}'); - final Exam exam = Exam(id, begin, end, subject ?? '', rooms, - examTypes[tableNum], course.faculty!); + final exam = Exam( + id, + begin, + end, + subject ?? '', + rooms, + examTypes[tableNum], + course.faculty!, + ); examsList.add(exam); }); diff --git a/uni/lib/controller/parsers/parser_fees.dart b/uni/lib/controller/parsers/parser_fees.dart index 239f5bdf2..ae87af351 100644 --- a/uni/lib/controller/parsers/parser_fees.dart +++ b/uni/lib/controller/parsers/parser_fees.dart @@ -1,33 +1,24 @@ import 'package:html/parser.dart' show parse; import 'package:http/http.dart' as http; -import 'dart:async'; /// Extracts the balance of the user's account from an HTTP [response]. -Future parseFeesBalance(http.Response response) async { +String parseFeesBalance(http.Response response) { final document = parse(response.body); - - final String? balanceString = - document.querySelector('span#span_saldo_total')?.text; - - final String balance = '$balanceString €'; - - return balance; + final balanceString = document.querySelector('span#span_saldo_total')?.text; + return '$balanceString €'; } /// Extracts the user's payment due date from an HTTP [response]. /// /// If there are no due payments, `Sem data` is returned. -Future parseFeesNextLimit(http.Response response) async { +DateTime? parseFeesNextLimit(http.Response response) { final document = parse(response.body); - final lines = document.querySelectorAll('#tab0 .tabela tr'); - if (lines.length < 2) { + try { + final limit = lines[1].querySelectorAll('.data')[1].text; + return DateTime.parse(limit); + } catch (_) { return null; } - final String limit = lines[1].querySelectorAll('.data')[1].text; - - //it's completly fine to throw an exeception if it fails, in this case, - //since probably sigarra is returning something we don't except - return DateTime.parse(limit); } diff --git a/uni/lib/controller/parsers/parser_library_occupation.dart b/uni/lib/controller/parsers/parser_library_occupation.dart index d8437dd9f..7f216537f 100644 --- a/uni/lib/controller/parsers/parser_library_occupation.dart +++ b/uni/lib/controller/parsers/parser_library_occupation.dart @@ -3,26 +3,29 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:uni/model/entities/library_occupation.dart'; +// ignore_for_file: avoid_dynamic_calls + Future parseLibraryOccupationFromSheets( - Response response) async { + Response response, +) async { final json = response.body.split('\n')[1]; // ignore first line - const toSkip = - 'google.visualization.Query.setResponse('; // this content should be ignored + const toSkip = 'google.visualization.Query.setResponse('; // should be ignored const numFloors = 6; - final LibraryOccupation occupation = LibraryOccupation(0, 0); + final occupation = LibraryOccupation(0, 0); final jsonDecoded = jsonDecode(json.substring(toSkip.length, json.length - 2)); - for (int i = 0; i < numFloors; i++) { - int floor, max; + for (var i = 0; i < numFloors; i++) { + int floor; + int max; try { - floor = jsonDecoded["table"]["rows"][i]["c"][0]["v"].toInt(); + floor = (jsonDecoded['table']['rows'][i]['c'][0]['v'] as double).toInt(); } catch (e) { floor = 0; } try { - max = jsonDecoded["table"]["rows"][i]["c"][1]["v"].toInt(); + max = (jsonDecoded['table']['rows'][i]['c'][1]['v'] as double).toInt(); } catch (e) { max = 0; } diff --git a/uni/lib/controller/parsers/parser_print_balance.dart b/uni/lib/controller/parsers/parser_print_balance.dart index ad8a94b22..a4a0b3a4d 100644 --- a/uni/lib/controller/parsers/parser_print_balance.dart +++ b/uni/lib/controller/parsers/parser_print_balance.dart @@ -1,15 +1,16 @@ +import 'dart:async'; + import 'package:html/parser.dart' show parse; import 'package:http/http.dart' as http; -import 'dart:async'; /// Extracts the print balance of the user's account from an HTTP [response]. Future getPrintsBalance(http.Response response) async { final document = parse(response.body); - final String? balanceString = + final balanceString = document.querySelector('div#conteudoinner > .info')?.text; - final String? balance = balanceString?.split(': ')[1]; + final balance = balanceString?.split(': ')[1]; return balance ?? ''; } diff --git a/uni/lib/controller/parsers/parser_references.dart b/uni/lib/controller/parsers/parser_references.dart index d539b3a80..f60fa8258 100644 --- a/uni/lib/controller/parsers/parser_references.dart +++ b/uni/lib/controller/parsers/parser_references.dart @@ -7,21 +7,21 @@ import 'package:uni/model/entities/reference.dart'; Future> parseReferences(http.Response response) async { final document = parse(response.body); - final List references = []; + final references = []; - final List rows = + final rows = document.querySelectorAll('div#tab0 > table.dadossz > tbody > tr'); if (rows.length > 1) { rows.sublist(1).forEach((Element tr) { - final List info = tr.querySelectorAll('td'); - final String description = info[0].text; - final DateTime limitDate = DateTime.parse(info[2].text); - final int entity = int.parse(info[3].text); - final int reference = int.parse(info[4].text); - final String formattedAmount = + final info = tr.querySelectorAll('td'); + final description = info[0].text; + final limitDate = DateTime.parse(info[2].text); + final entity = int.parse(info[3].text); + final reference = int.parse(info[4].text); + final formattedAmount = info[5].text.replaceFirst(',', '.').replaceFirst('€', ''); - final double amount = double.parse(formattedAmount); + final amount = double.parse(formattedAmount); references .add(Reference(description, limitDate, entity, reference, amount)); }); diff --git a/uni/lib/controller/parsers/parser_restaurants.dart b/uni/lib/controller/parsers/parser_restaurants.dart index 204afd3d5..69cd921c9 100644 --- a/uni/lib/controller/parsers/parser_restaurants.dart +++ b/uni/lib/controller/parsers/parser_restaurants.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:html/dom.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; @@ -9,35 +8,35 @@ import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/utils/day_of_week.dart'; +// ignore_for_file: avoid_dynamic_calls + /// Reads restaurants's menu from /feup/pt/CANTINA.EMENTASHOW List getRestaurantsFromHtml(Response response) { final document = parse(response.body); //Get restaurant reference number and name - final List restaurantsHtml = - document.querySelectorAll('#conteudoinner ul li > a'); + final restaurantsHtml = document.querySelectorAll('#conteudoinner ul li > a'); - final List> restaurantsTuple = - restaurantsHtml.map((restaurantHtml) { - final String name = restaurantHtml.text; - final String? ref = restaurantHtml.attributes['href']?.replaceAll('#', ''); + final restaurantsTuple = restaurantsHtml.map((restaurantHtml) { + final name = restaurantHtml.text; + final ref = restaurantHtml.attributes['href']?.replaceAll('#', ''); return Tuple2(ref ?? '', name); }).toList(); //Get restaurant meals and create the Restaurant class - final List restaurants = restaurantsTuple.map((restaurantTuple) { - final List meals = []; + final restaurants = restaurantsTuple.map((restaurantTuple) { + final meals = []; - final Element? referenceA = + final referenceA = document.querySelector('a[name="${restaurantTuple.item1}"]'); - Element? next = referenceA?.nextElementSibling; + var next = referenceA?.nextElementSibling; - final DateFormat format = DateFormat('d-M-y'); + final format = DateFormat('d-M-y'); while (next != null && next.attributes['name'] == null) { next = next.nextElementSibling; if (next!.classes.contains('dados')) { //It's the menu table - final List rows = next.querySelectorAll('tr'); + final rows = next.querySelectorAll('tr'); //Check if is empty if (rows.length <= 1) { break; @@ -48,13 +47,13 @@ List getRestaurantsFromHtml(Response response) { DayOfWeek? dayOfWeek; String? type; DateTime? date; - final List columns = row.querySelectorAll('td'); + final columns = row.querySelectorAll('td'); - for (var column in columns) { - final String value = column.text; - final String? header = column.attributes['headers']; + for (final column in columns) { + final value = column.text; + final header = column.attributes['headers']; if (header == 'Data') { - final DayOfWeek? d = parseDayOfWeek(value); + final d = parseDayOfWeek(value); if (d == null) { //It's a date date = format.parseUtc(value); @@ -63,7 +62,7 @@ List getRestaurantsFromHtml(Response response) { } } else { type = document.querySelector('#$header')?.text; - final Meal meal = Meal(type ?? '', value, dayOfWeek!, date!); + final meal = Meal(type ?? '', value, dayOfWeek!, date!); meals.add(meal); } } @@ -71,37 +70,47 @@ List getRestaurantsFromHtml(Response response) { break; } } - return Restaurant(null, restaurantTuple.item2, restaurantTuple.item1, - meals: meals); + return Restaurant( + null, + restaurantTuple.item2, + restaurantTuple.item1, + meals: meals, + ); }).toList(); return restaurants; } -Restaurant getRestaurantFromGSheets(Response response, String restaurantName, - {bool isDinner = false}) { +Restaurant getRestaurantFromGSheets( + Response response, + String restaurantName, { + bool isDinner = false, +}) { // Ignore beginning of response: "/*O_o*/\ngoogle.visualization.Query.setResponse(" // Ignore the end of the response: ");" // Check the structure by accessing the link: // https://docs.google.com/spreadsheets/d/1TJauM0HwIf2RauQU2GmhdZZ1ZicFLMHuBkxWwVOw3Q4/gviz/tq?tqx=out:json&sheet=Cantina%20de%20Engenharia&range=A:D final jsonString = response.body.substring( - response.body.indexOf('(') + 1, response.body.lastIndexOf(')')); + response.body.indexOf('(') + 1, + response.body.lastIndexOf(')'), + ); final parsedJson = jsonDecode(jsonString); - final List mealsList = []; + final mealsList = []; - final DateFormat format = DateFormat('d/M/y'); - for (var row in parsedJson['table']['rows']) { + final format = DateFormat('d/M/y'); + for (final row in parsedJson['table']['rows'] as List) { final cellList = row['c']; if ((cellList[1]['v'] == 'Almoço' && isDinner) || (cellList[1]['v'] != 'Almoço' && !isDinner)) { continue; } - final Meal meal = Meal( - cellList[2]['v'], - cellList[3]['v'], - DayOfWeek.values[format.parseUtc(cellList[0]['f']).weekday - 1], - format.parseUtc(cellList[0]['f'])); + final meal = Meal( + cellList[2]['v'] as String, + cellList[3]['v'] as String, + DayOfWeek.values[format.parseUtc(cellList[0]['f'] as String).weekday - 1], + format.parseUtc(cellList[0]['f'] as String), + ); mealsList.add(meal); } diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 6fd72a55e..2a98c4853 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -1,12 +1,15 @@ 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'; -Future> parseScheduleMultipleRequests(responses) async { - List lectures = []; - for (var response in responses) { +Future> parseScheduleMultipleRequests( + List responses, +) async { + var lectures = []; + for (final response in responses) { lectures += await parseSchedule(response); } return lectures; @@ -17,45 +20,49 @@ Future> parseScheduleMultipleRequests(responses) async { /// /// This function parses a JSON object. Future> parseSchedule(http.Response response) async { - final Set lectures = {}; + final lectures = {}; - final json = jsonDecode(response.body); - - final schedule = json['horario']; + final json = jsonDecode(response.body) as Map; + final schedule = json['horario'] as List; for (var lecture in schedule) { - final int day = (lecture['dia'] - 2) % + lecture = lecture as Map; + final day = ((lecture['dia'] as int) - 2) % 7; // Api: monday = 2, Lecture.dart class: monday = 0 - final int secBegin = lecture['hora_inicio']; - final String subject = lecture['ucurr_sigla']; - final String typeClass = lecture['tipo']; - final int blocks = (lecture['aula_duracao'] * 2).round(); - final String room = lecture['sala_sigla'].replaceAll(RegExp('\\+'), '\n'); - final String teacher = lecture['doc_sigla']; - final String classNumber = lecture['turma_sigla']; - final int occurrId = lecture['ocorrencia_id']; - - final DateTime monday = DateTime.now().getClosestMonday(); - - final Lecture lec = Lecture.fromApi( - subject, - typeClass, - monday.add(Duration(days: day, seconds: secBegin)), - blocks, - room, - teacher, - classNumber, - occurrId); + final secBegin = lecture['hora_inicio'] as int; + final subject = lecture['ucurr_sigla'] as String; + final typeClass = lecture['tipo'] as String; + // TODO(luisd): this was marked as a double on the develop branch but the + // tests' example api returns an integer. At the moment there are no + // classes so I can't test this. + final blocks = (lecture['aula_duracao'] as int) * 2; + final room = + (lecture['sala_sigla'] as String).replaceAll(RegExp(r'\+'), '\n'); + final teacher = lecture['doc_sigla'] as String; + 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)), + blocks, + room, + teacher, + classNumber, + occurrId, + ); lectures.add(lec); } - final lecturesList = lectures.toList(); - lecturesList.sort((a, b) => a.compare(b)); + final lecturesList = lectures.toList()..sort((a, b) => a.compare(b)); if (lecturesList.isEmpty) { return Future.error(Exception('Found empty schedule')); - } else { - return lecturesList; } + + return lecturesList; } diff --git a/uni/lib/controller/parsers/parser_schedule_html.dart b/uni/lib/controller/parsers/parser_schedule_html.dart index dc2e49348..88ac5ce8c 100644 --- a/uni/lib/controller/parsers/parser_schedule_html.dart +++ b/uni/lib/controller/parsers/parser_schedule_html.dart @@ -1,72 +1,80 @@ import 'dart:async'; -import 'package:http/http.dart' as http; -import 'package:html/parser.dart' show parse; + import 'package:html/dom.dart'; -import 'package:http/http.dart'; +import 'package:html/parser.dart' show parse; +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'; Future> getOverlappedClasses( - Session session, Document document) async { - final List lecturesList = []; + Session session, + Document document, +) async { + final lecturesList = []; - final DateTime monday = DateTime.now().getClosestMonday(); + final monday = DateTime.now().getClosestMonday(); final overlappingClasses = document.querySelectorAll('.dados > tbody > .d'); for (final element in overlappingClasses) { - final String? subject = element.querySelector('acronym > a')?.text; - final String? typeClass = element + final subject = element.querySelector('acronym > a')?.text; + final typeClass = element .querySelector('td[headers=t1]') ?.nodes[2] .text ?.trim() - .replaceAll(RegExp(r'[()]+'), ''); - final String? textDay = element.querySelector('td[headers=t2]')?.text; - final int? aDay = document + .replaceAll(RegExp('[()]+'), ''); + final textDay = element.querySelector('td[headers=t2]')?.text; + final aDay = document .querySelector('.horario > tbody > tr:first-child') ?.children .indexWhere((element) => element.text == textDay); - final int day = aDay != null ? aDay - 1 : 0; - final String? startTime = element.querySelector('td[headers=t3]')?.text; - final String? room = element.querySelector('td[headers=t4] > a')?.text; - final String? teacher = element.querySelector('td[headers=t5] > a')?.text; - final String? classNumber = - element.querySelector('td[headers=t6] > a')?.text; + final day = aDay != null ? aDay - 1 : 0; + 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; + final classNumber = element.querySelector('td[headers=t6] > a')?.text; try { - final DateTime fullStartTime = monday.add(Duration( + final fullStartTime = monday.add( + Duration( days: day, hours: int.parse(startTime!.substring(0, 2)), - minutes: int.parse(startTime.substring(3, 5)))); - final String? link = + minutes: int.parse(startTime.substring(3, 5)), + ), + ); + final link = element.querySelector('td[headers=t6] > a')?.attributes['href']; if (link == null) { throw Exception(); } - final Response response = - await NetworkRouter.getWithCookies(link, {}, session); + final response = await NetworkRouter.getWithCookies(link, {}, session); final classLectures = await getScheduleFromHtml(response, session); - lecturesList.add(classLectures - .where((element) => - element.subject == subject && element.startTime == fullStartTime) - .first); + lecturesList.add( + classLectures + .where( + (element) => + element.subject == subject && + element.startTime == fullStartTime, + ) + .first, + ); } catch (e) { - final Lecture lect = Lecture.fromHtml( - subject!, - typeClass!, - monday.add(Duration(days: day)), - startTime!, - 0, - room!, - teacher!, - classNumber!, - -1); + final lect = Lecture.fromHtml( + subject!, + typeClass!, + monday.add(Duration(days: day)), + startTime!, + 0, + room!, + teacher!, + classNumber!, + -1, + ); lecturesList.add(lect); } } @@ -78,18 +86,20 @@ Future> getOverlappedClasses( /// /// This function parses the schedule's HTML page. Future> getScheduleFromHtml( - http.Response response, Session session) async { + http.Response response, + Session session, +) async { final document = parse(response.body); var semana = [0, 0, 0, 0, 0, 0]; - final List lecturesList = []; + final lecturesList = []; - final DateTime monday = DateTime.now().getClosestMonday(); + final monday = DateTime.now().getClosestMonday(); document.querySelectorAll('.horario > tbody > tr').forEach((Element element) { if (element.getElementsByClassName('horas').isNotEmpty) { var day = 0; - final List children = element.children; + final children = element.children; for (var i = 1; i < children.length; i++) { for (var d = day; d < semana.length; d++) { if (semana[d] == 0) { @@ -99,8 +109,7 @@ Future> getScheduleFromHtml( } final clsName = children[i].className; if (clsName == 'TE' || clsName == 'TP' || clsName == 'PL') { - final String? subject = - children[i].querySelector('b > acronym > a')?.text; + final subject = children[i].querySelector('b > acronym > a')?.text; if (subject == null) return; String? classNumber; @@ -109,37 +118,38 @@ Future> getScheduleFromHtml( if (classNumber == null) return; } - final Element? rowSmall = - children[i].querySelector('table > tbody > tr'); - final String? room = rowSmall?.querySelector('td > a')?.text; - final String? teacher = rowSmall?.querySelector('td.textod a')?.text; + final rowSmall = children[i].querySelector('table > tbody > tr'); + final room = rowSmall?.querySelector('td > a')?.text; + final teacher = rowSmall?.querySelector('td.textod a')?.text; - final String typeClass = clsName; - final int blocks = int.parse(children[i].attributes['rowspan']!); - final String startTime = children[0].text.substring(0, 5); + final typeClass = clsName; + final blocks = int.parse(children[i].attributes['rowspan']!); + final startTime = children[0].text.substring(0, 5); semana[day] += blocks; - final Lecture lect = Lecture.fromHtml( - subject, - typeClass, - monday.add(Duration(days: day)), - startTime, - blocks, - room ?? '', - teacher ?? '', - classNumber ?? '', - -1); + final lect = Lecture.fromHtml( + subject, + typeClass, + monday.add(Duration(days: day)), + startTime, + blocks, + room ?? '', + teacher ?? '', + classNumber ?? '', + -1, + ); lecturesList.add(lect); } day++; } - semana = semana.expand((i) => [(i - 1) < 0 ? 0 : i - 1]).toList(); + semana = semana.expand((i) => [if ((i - 1) < 0) 0 else i - 1]).toList(); } }); - lecturesList.addAll(await getOverlappedClasses(session, document)); - lecturesList.sort((a, b) => a.compare(b)); + lecturesList + ..addAll(await getOverlappedClasses(session, document)) + ..sort((a, b) => a.compare(b)); return lecturesList; } diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 785cb030b..702a8d2b4 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -49,73 +49,89 @@ SentryEvent? beforeSend(SentryEvent event) { Future main() async { final stateProviders = StateProviders( - LectureProvider(), - ExamProvider(), - BusStopProvider(), - RestaurantProvider(), - ProfileProvider(), - CourseUnitsInfoProvider(), - SessionProvider(), - CalendarProvider(), - LibraryOccupationProvider(), - FacultyLocationsProvider(), - HomePageProvider(), - ReferenceProvider()); + LectureProvider(), + ExamProvider(), + BusStopProvider(), + RestaurantProvider(), + ProfileProvider(), + CourseUnitsInfoProvider(), + SessionProvider(), + CalendarProvider(), + LibraryOccupationProvider(), + FacultyLocationsProvider(), + HomePageProvider(), + ReferenceProvider(), + ); WidgetsFlutterBinding.ensureInitialized(); - await Workmanager().initialize(workerStartCallback, - isInDebugMode: - !kReleaseMode // run workmanager in debug mode when app is in debug mode - ); + await Workmanager().initialize( + workerStartCallback, + isInDebugMode: !kReleaseMode, + // run workmanager in debug mode when app is in debug mode + ); await dotenv - .load(fileName: "assets/env/.env", isOptional: true) + .load(fileName: 'assets/env/.env', isOptional: true) .onError((error, stackTrace) { - Logger().e("Error loading .env file: $error", error, stackTrace); + Logger().e('Error loading .env file: $error', error, stackTrace); }); final savedTheme = await AppSharedPreferences.getThemeMode(); - await SentryFlutter.init((options) { - options.dsn = - 'https://a2661645df1c4992b24161010c5e0ecb@o553498.ingest.sentry.io/5680848'; - }, - 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), - ], - child: ChangeNotifierProvider( - create: (_) => ThemeNotifier(savedTheme), - child: const MyApp(), - ))) - }); + await SentryFlutter.init( + (options) { + options.dsn = + 'https://a2661645df1c4992b24161010c5e0ecb@o553498.ingest.sentry.io/5680848'; + }, + 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, + ), + ], + child: ChangeNotifierProvider( + create: (_) => ThemeNotifier(savedTheme), + child: const MyApp(), + ), + ), + ) + }, + ); } /// Manages the state of the app @@ -139,57 +155,77 @@ class MyAppState extends State { return Consumer( builder: (context, themeNotifier, _) => MaterialApp( - title: 'uni', - theme: applicationLightTheme, - darkTheme: applicationDarkTheme, - themeMode: themeNotifier.getTheme(), - home: const SplashScreen(), - navigatorKey: NavigationService.navigatorKey, - onGenerateRoute: (RouteSettings settings) { - final Map> transitions = { - '/${DrawerItem.navPersonalArea.title}': - PageTransition.makePageTransition( - page: const HomePageView(), settings: settings), - '/${DrawerItem.navSchedule.title}': - PageTransition.makePageTransition( - page: const SchedulePage(), settings: settings), - '/${DrawerItem.navExams.title}': - PageTransition.makePageTransition( - page: const ExamsPageView(), settings: settings), - '/${DrawerItem.navStops.title}': - PageTransition.makePageTransition( - page: const BusStopNextArrivalsPage(), - settings: settings), - '/${DrawerItem.navCourseUnits.title}': - PageTransition.makePageTransition( - page: const CourseUnitsPageView(), settings: settings), - '/${DrawerItem.navLocations.title}': - PageTransition.makePageTransition( - page: const LocationsPage(), settings: settings), - '/${DrawerItem.navRestaurants.title}': - PageTransition.makePageTransition( - page: const RestaurantPageView(), settings: settings), - '/${DrawerItem.navCalendar.title}': - PageTransition.makePageTransition( - page: const CalendarPageView(), settings: settings), - '/${DrawerItem.navLibrary.title}': - PageTransition.makePageTransition( - page: const LibraryPageView(), settings: settings), - '/${DrawerItem.navUsefulInfo.title}': - PageTransition.makePageTransition( - page: const UsefulInfoPageView(), settings: settings), - '/${DrawerItem.navAbout.title}': - PageTransition.makePageTransition( - page: const AboutPageView(), settings: settings), - '/${DrawerItem.navBugReport.title}': - PageTransition.makePageTransition( - page: const BugReportPageView(), - settings: settings, - maintainState: false), - '/${DrawerItem.navLogOut.title}': LogoutRoute.buildLogoutRoute() - }; - return transitions[settings.name]; - }), + title: 'uni', + theme: applicationLightTheme, + darkTheme: applicationDarkTheme, + themeMode: themeNotifier.getTheme(), + home: const SplashScreen(), + navigatorKey: NavigationService.navigatorKey, + onGenerateRoute: (RouteSettings settings) { + final transitions = >{ + '/${DrawerItem.navPersonalArea.title}': + PageTransition.makePageTransition( + page: const HomePageView(), + settings: settings, + ), + '/${DrawerItem.navSchedule.title}': + PageTransition.makePageTransition( + page: const SchedulePage(), + settings: settings, + ), + '/${DrawerItem.navExams.title}': PageTransition.makePageTransition( + page: const ExamsPageView(), + settings: settings, + ), + '/${DrawerItem.navStops.title}': PageTransition.makePageTransition( + page: const BusStopNextArrivalsPage(), + settings: settings, + ), + '/${DrawerItem.navCourseUnits.title}': + PageTransition.makePageTransition( + page: const CourseUnitsPageView(), + settings: settings, + ), + '/${DrawerItem.navLocations.title}': + PageTransition.makePageTransition( + page: const LocationsPage(), + settings: settings, + ), + '/${DrawerItem.navRestaurants.title}': + PageTransition.makePageTransition( + page: const RestaurantPageView(), + settings: settings, + ), + '/${DrawerItem.navCalendar.title}': + PageTransition.makePageTransition( + page: const CalendarPageView(), + settings: settings, + ), + '/${DrawerItem.navLibrary.title}': + PageTransition.makePageTransition( + page: const LibraryPageView(), + settings: settings, + ), + '/${DrawerItem.navUsefulInfo.title}': + PageTransition.makePageTransition( + page: const UsefulInfoPageView(), + settings: settings, + ), + '/${DrawerItem.navAbout.title}': PageTransition.makePageTransition( + page: const AboutPageView(), + settings: settings, + ), + '/${DrawerItem.navBugReport.title}': + PageTransition.makePageTransition( + page: const BugReportPageView(), + settings: settings, + maintainState: false, + ), + '/${DrawerItem.navLogOut.title}': LogoutRoute.buildLogoutRoute() + }; + return transitions[settings.name]; + }, + ), ); } } diff --git a/uni/lib/model/entities/bug_report.dart b/uni/lib/model/entities/bug_report.dart index 9596c7eeb..075f5306d 100644 --- a/uni/lib/model/entities/bug_report.dart +++ b/uni/lib/model/entities/bug_report.dart @@ -1,13 +1,14 @@ -/// Stores information about Bug Report import 'package:tuple/tuple.dart'; class BugReport { + BugReport(this.title, this.text, this.email, this.bugLabel, this.faculties); + final String title; final String text; final String email; final Tuple2? bugLabel; final List faculties; - BugReport(this.title, this.text, this.email, this.bugLabel, this.faculties); + Map toMap() => { 'title': title, 'text': text, diff --git a/uni/lib/model/entities/bus.dart b/uni/lib/model/entities/bus.dart index f69675e4d..ab885019a 100644 --- a/uni/lib/model/entities/bus.dart +++ b/uni/lib/model/entities/bus.dart @@ -3,15 +3,15 @@ /// Stores the bus code (`busCode`), the `destination` of the bus /// and its `direction`. class Bus { + Bus({ + required this.busCode, + required this.destination, + this.direction = false, + }); String busCode; String destination; bool direction; - Bus( - {required this.busCode, - required this.destination, - this.direction = false}); - /// Converts a [Bus] instance to a map. /// /// The map contents are the `busCode`, diff --git a/uni/lib/model/entities/bus_stop.dart b/uni/lib/model/entities/bus_stop.dart index 38017bd19..ba79553b2 100644 --- a/uni/lib/model/entities/bus_stop.dart +++ b/uni/lib/model/entities/bus_stop.dart @@ -2,12 +2,12 @@ import 'package:uni/model/entities/trip.dart'; /// Stores information about a bus stop. class BusStopData { + BusStopData({ + required this.configuredBuses, + this.favorited = false, + this.trips = const [], + }); final Set configuredBuses; bool favorited; List trips; - - BusStopData( - {required this.configuredBuses, - this.favorited = false, - this.trips = const []}); } diff --git a/uni/lib/model/entities/calendar_event.dart b/uni/lib/model/entities/calendar_event.dart index eebe459cd..733a3909a 100644 --- a/uni/lib/model/entities/calendar_event.dart +++ b/uni/lib/model/entities/calendar_event.dart @@ -1,10 +1,9 @@ /// An event in the school calendar class CalendarEvent { - String name; - String date; - /// Creates an instance of the class [CalendarEvent] CalendarEvent(this.name, this.date); + String name; + String date; /// Converts the event into a map Map toMap() { diff --git a/uni/lib/model/entities/course.dart b/uni/lib/model/entities/course.dart index 8ca27a8e0..1c739fb42 100644 --- a/uni/lib/model/entities/course.dart +++ b/uni/lib/model/entities/course.dart @@ -8,6 +8,29 @@ /// - The date of the `firstEnrollment` /// - The course `state` class Course { + Course({ + required this.id, + this.festId = 0, + this.name, + this.abbreviation, + this.currYear, + this.firstEnrollment, + this.state, + this.faculty, + this.finishedEcts, + this.currentAverage, + }); + + /// Creates a new instance from a JSON object. + Course.fromJson(Map data) + : id = data['cur_id'] as int, + festId = data['fest_id'] as int, + name = data['cur_nome'] as String?, + currYear = data['ano_curricular'] as String?, + firstEnrollment = data['fest_a_lect_1_insc'] as int, + abbreviation = data['abbreviation'] as String?, + faculty = data['inst_sigla']?.toString().toLowerCase(); + final int id; final int? festId; final String? name; @@ -19,30 +42,6 @@ class Course { num? finishedEcts; num? currentAverage; - Course( - {required this.id, - this.festId = 0, - this.name, - this.abbreviation, - this.currYear, - this.firstEnrollment, - this.state, - this.faculty, - this.finishedEcts, - this.currentAverage}); - - /// Creates a new instance from a JSON object. - static Course fromJson(dynamic data) { - return Course( - id: data['cur_id'], - festId: data['fest_id'], - name: data['cur_nome'], - currYear: data['ano_curricular'], - firstEnrollment: data['fest_a_lect_1_insc'], - abbreviation: data['abbreviation'], - faculty: data['inst_sigla'].toString().toLowerCase()); - } - /// Converts this course to a map. Map toMap() { return { diff --git a/uni/lib/model/entities/course_units/course_unit.dart b/uni/lib/model/entities/course_units/course_unit.dart index 675e86096..8ac041fd5 100644 --- a/uni/lib/model/entities/course_units/course_unit.dart +++ b/uni/lib/model/entities/course_units/course_unit.dart @@ -1,5 +1,23 @@ /// Stores information about a course unit. class CourseUnit { + CourseUnit({ + required this.abbreviation, + required this.name, + required this.occurrId, + this.id = 0, + this.code = '', + this.curricularYear, + this.semesterCode, + this.semesterName, + this.type, + this.status, + this.grade, + this.ectsGrade, + this.result, + this.ects, + this.schoolYear, + }); + int id; String code; String abbreviation; @@ -14,45 +32,33 @@ class CourseUnit { String? ectsGrade; String? result; num? ects; - String? schoolYear; - - CourseUnit( - {this.id = 0, - this.code = '', - required this.abbreviation, - required this.name, - this.curricularYear, - required this.occurrId, - this.semesterCode, - this.semesterName, - this.type, - this.status, - this.grade, - this.ectsGrade, - this.result, - this.ects, - this.schoolYear}); + String? schoolYear; // e.g. 2020/2021 /// Creates a new instance from a JSON object. - static CourseUnit? fromJson(dynamic data) { + static CourseUnit? fromJson(Map data) { if (data['ucurr_id'] == null) { return null; } + return CourseUnit( - id: data['ucurr_id'], - code: data['ucurr_codigo'], - abbreviation: data['ucurr_sigla'], - name: data['ucurr_nome'], - curricularYear: data['ano'], - occurrId: data['ocorr_id'], - semesterCode: data['per_codigo'], - semesterName: data['per_nome'], - type: data['tipo'], - status: data['estado'], - grade: data['resultado_melhor'], - ectsGrade: data['resultado_ects'], - result: data['resultado_insc'], - ects: data['creditos_ects']); + id: data['ucurr_id'] as int, + code: data['ucurr_codigo'] as String, + abbreviation: data['ucurr_sigla'] as String, + name: data['ucurr_nome'] as String, + curricularYear: data['ano'] as int, + occurrId: data['ocorr_id'] as int, + semesterCode: data['per_codigo'] as String?, + semesterName: data['per_nome'] as String?, + type: data['tipo'] as String?, + status: data['estado'] as String?, + grade: data['resultado_melhor'] as String?, + ectsGrade: data['resultado_ects'] as String?, + result: data['resultado_insc'] as String?, + ects: data['creditos_ects'] as num?, + schoolYear: data['a_lectivo'] == null + ? null + : toSchoolYear(data['a_lectivo'] as int), + ); } Map toMap() { @@ -78,4 +84,8 @@ class CourseUnit { bool enrollmentIsValid() { return status == 'V'; } + + static String toSchoolYear(int year) { + return '$year/${year + 1}'; + } } diff --git a/uni/lib/model/entities/course_units/course_unit_class.dart b/uni/lib/model/entities/course_units/course_unit_class.dart index d1948287f..aeb49de10 100644 --- a/uni/lib/model/entities/course_units/course_unit_class.dart +++ b/uni/lib/model/entities/course_units/course_unit_class.dart @@ -1,16 +1,20 @@ class CourseUnitClass { + CourseUnitClass(this.className, this.students); String className; List students; - CourseUnitClass(this.className, this.students); } class CourseUnitStudent { + CourseUnitStudent( + this.name, + this.number, + this.mail, + this.photo, + this.profile, + ); String name; int number; String mail; Uri photo; Uri profile; - - CourseUnitStudent( - this.name, this.number, this.mail, this.photo, this.profile); } diff --git a/uni/lib/model/entities/course_units/course_unit_sheet.dart b/uni/lib/model/entities/course_units/course_unit_sheet.dart index ecf6b7158..e57f4012c 100644 --- a/uni/lib/model/entities/course_units/course_unit_sheet.dart +++ b/uni/lib/model/entities/course_units/course_unit_sheet.dart @@ -1,5 +1,4 @@ class CourseUnitSheet { - Map sections; - CourseUnitSheet(this.sections); + Map sections; } diff --git a/uni/lib/model/entities/exam.dart b/uni/lib/model/entities/exam.dart index eec51bce7..b8d8ef370 100644 --- a/uni/lib/model/entities/exam.dart +++ b/uni/lib/model/entities/exam.dart @@ -2,34 +2,36 @@ import 'package:intl/intl.dart'; import 'package:logger/logger.dart'; enum WeekDays { - monday("Segunda"), - tuesday("Terça"), - wednesday("Quarta"), - thursday("Quinta"), - friday("Sexta"), - saturday("Sábado"), - sunday("Domingo"); + monday('Segunda'), + tuesday('Terça'), + wednesday('Quarta'), + thursday('Quinta'), + friday('Sexta'), + saturday('Sábado'), + sunday('Domingo'); - final String day; const WeekDays(this.day); + + final String day; } enum Months { - january("janeiro"), - february("fevereiro"), - march("março"), - april("abril"), - may("maio"), - june("junho"), - july("julho"), - august("agosto"), - september("setembro"), - october("outubro"), - november("novembro"), - december("dezembro"); + january('janeiro'), + february('fevereiro'), + march('março'), + april('abril'), + may('maio'), + june('junho'), + july('julho'), + august('agosto'), + september('setembro'), + october('outubro'), + november('novembro'), + december('dezembro'); - final String month; const Months(this.month); + + final String month; } /// Manages a generic Exam. @@ -39,8 +41,29 @@ enum Months { /// - The Exam `subject` /// - A List with the `rooms` in which the Exam takes place /// - The Exam `type` - class Exam { + Exam( + this.id, + this.begin, + this.end, + this.subject, + this.rooms, + this.type, + this.faculty, + ); + + Exam.secConstructor( + this.id, + this.subject, + this.begin, + this.end, + String rooms, + this.type, + this.faculty, + ) { + this.rooms = rooms.split(','); + } + late final DateTime begin; late final DateTime end; late final String id; @@ -57,23 +80,15 @@ class Exam { 'Port.Est.Especiais': 'EE', 'Exames ao abrigo de estatutos especiais': 'EAE' }; - - Exam(this.id, this.begin, this.end, this.subject, this.rooms, this.type, - this.faculty); static List displayedTypes = types.keys.toList().sublist(0, 4); - Exam.secConstructor(this.id, this.subject, this.begin, this.end, String rooms, - this.type, this.faculty) { - this.rooms = rooms.split(','); - } - /// Converts this exam to a map. Map toMap() { return { 'id': id, 'subject': subject, - 'begin': DateFormat("yyyy-MM-dd HH:mm:ss").format(begin), - 'end': DateFormat("yyyy-MM-dd HH:mm:ss").format(end), + 'begin': DateFormat('yyyy-MM-dd HH:mm:ss').format(begin), + 'end': DateFormat('yyyy-MM-dd HH:mm:ss').format(end), 'rooms': rooms.join(','), 'examType': type, 'faculty': faculty @@ -95,7 +110,7 @@ class Exam { @override String toString() { - return '''$id - $subject - ${begin.year.toString()} - $month - ${begin.day} - $beginTime-$endTime - $type - $rooms - $weekDay'''; + return '''$id - $subject - ${begin.year} - $month - ${begin.day} - $beginTime-$endTime - $type - $rooms - $weekDay'''; } /// Prints the data in this exam to the [Logger] with an INFO level. @@ -110,8 +125,8 @@ class Exam { @override int get hashCode => id.hashCode; - static getExamTypeLong(String abr) { - final Map reversed = types.map((k, v) => MapEntry(v, k)); + static String? getExamTypeLong(String abr) { + final reversed = types.map((k, v) => MapEntry(v, k)); return reversed[abr]; } } diff --git a/uni/lib/model/entities/lecture.dart b/uni/lib/model/entities/lecture.dart index 66eddf33e..8ff9da9db 100644 --- a/uni/lib/model/entities/lecture.dart +++ b/uni/lib/model/entities/lecture.dart @@ -2,75 +2,107 @@ import 'package:logger/logger.dart'; /// Stores information about a lecture. class Lecture { - String subject; - String typeClass; - String room; - String teacher; - String classNumber; - DateTime startTime; - DateTime endTime; - int blocks; - int occurrId; - /// Creates an instance of the class [Lecture]. - Lecture(this.subject, this.typeClass, this.startTime, this.endTime, - this.blocks, this.room, this.teacher, this.classNumber, this.occurrId); + Lecture( + this.subject, + this.typeClass, + this.startTime, + this.endTime, + this.blocks, + this.room, + this.teacher, + this.classNumber, + this.occurrId, + ); factory Lecture.fromApi( - String subject, - String typeClass, - DateTime startTime, - int blocks, - String room, - String teacher, - String classNumber, - int occurrId) { + String subject, + String typeClass, + DateTime startTime, + int blocks, + String room, + String teacher, + String classNumber, + int occurrId, + ) { final endTime = startTime.add(Duration(seconds: 60 * 30 * blocks)); - final lecture = Lecture(subject, typeClass, startTime, endTime, blocks, - room, teacher, classNumber, occurrId); + final lecture = Lecture( + subject, + typeClass, + startTime, + endTime, + blocks, + room, + teacher, + classNumber, + occurrId, + ); return lecture; } factory Lecture.fromHtml( - String subject, - String typeClass, - DateTime day, - String startTime, - int blocks, - String room, - String teacher, - String classNumber, - int occurrId) { + String subject, + String typeClass, + DateTime day, + String startTime, + int blocks, + String room, + String teacher, + String classNumber, + int occurrId, + ) { final startTimeHours = int.parse(startTime.substring(0, 2)); final startTimeMinutes = int.parse(startTime.substring(3, 5)); final endTimeHours = (startTimeMinutes + (blocks * 30)) ~/ 60 + startTimeHours; final endTimeMinutes = (startTimeMinutes + (blocks * 30)) % 60; return Lecture( - subject, - typeClass, - day.add(Duration(hours: startTimeHours, minutes: startTimeMinutes)), - day.add(Duration( - hours: startTimeMinutes + endTimeHours, - minutes: startTimeMinutes + endTimeMinutes)), - blocks, - room, - teacher, - classNumber, - occurrId); + subject, + typeClass, + day.add(Duration(hours: startTimeHours, minutes: startTimeMinutes)), + day.add( + Duration( + hours: startTimeMinutes + endTimeHours, + minutes: startTimeMinutes + endTimeMinutes, + ), + ), + blocks, + room, + teacher, + classNumber, + occurrId, + ); } /// Clones a lecture from the api. - static Lecture clone(Lecture lec) { - return Lecture.fromApi(lec.subject, lec.typeClass, lec.startTime, - lec.blocks, lec.room, lec.teacher, lec.classNumber, lec.occurrId); + factory Lecture.clone(Lecture lec) { + return Lecture.fromApi( + lec.subject, + lec.typeClass, + lec.startTime, + lec.blocks, + lec.room, + lec.teacher, + lec.classNumber, + lec.occurrId, + ); } /// Clones a lecture from the html. - static Lecture cloneHtml(Lecture lec) { + factory Lecture.cloneHtml(Lecture lec) { return Lecture.clone(lec); } + String subject; + String typeClass; + String room; + String teacher; + String classNumber; + DateTime startTime; + DateTime endTime; + int blocks; + int occurrId; + /// Converts this lecture to a map. Map toMap() { return { @@ -86,13 +118,14 @@ class Lecture { } /// Prints the data in this lecture to the [Logger] with an INFO level. - printLecture() { + void printLecture() { Logger().i(toString()); } @override String toString() { - return "$subject $typeClass\n$startTime $endTime $blocks blocos\n $room $teacher\n"; + return '$subject $typeClass\n$startTime $endTime $blocks blocos\n $room ' + '$teacher\n'; } /// Compares the date and time of two lectures. @@ -101,11 +134,20 @@ class Lecture { } @override - int get hashCode => Object.hash(subject, startTime, endTime, typeClass, room, - teacher, startTime, blocks, occurrId); + int get hashCode => Object.hash( + subject, + startTime, + endTime, + typeClass, + room, + teacher, + startTime, + blocks, + occurrId, + ); @override - bool operator ==(other) => + bool operator ==(Object other) => other is Lecture && subject == other.subject && startTime == other.startTime && diff --git a/uni/lib/model/entities/library_occupation.dart b/uni/lib/model/entities/library_occupation.dart index 763f7c2e0..bb299bbda 100644 --- a/uni/lib/model/entities/library_occupation.dart +++ b/uni/lib/model/entities/library_occupation.dart @@ -1,12 +1,11 @@ /// Overall occupation of the library class LibraryOccupation { - late int occupation; - late int capacity; - late List floors; - LibraryOccupation(this.occupation, this.capacity) { floors = []; } + late int occupation; + late int capacity; + late List floors; void addFloor(FloorOccupation floor) { floors.add(floor); @@ -27,19 +26,18 @@ class LibraryOccupation { /// Occupation values of a single floor class FloorOccupation { + FloorOccupation(this.number, this.occupation, this.capacity); final int number; final int occupation; final int capacity; - FloorOccupation(this.number, this.occupation, this.capacity); - int get percentage { if (capacity == 0) return 0; return (occupation * 100 / capacity).round(); } Map toMap() { - final Map map = { + final map = { 'number': number, 'occupation': occupation, 'capacity': capacity, diff --git a/uni/lib/model/entities/location.dart b/uni/lib/model/entities/location.dart index 6af021369..bb716e178 100644 --- a/uni/lib/model/entities/location.dart +++ b/uni/lib/model/entities/location.dart @@ -1,14 +1,14 @@ +import 'package:uni/model/entities/locations/atm.dart'; import 'package:uni/model/entities/locations/coffee_machine.dart'; +import 'package:uni/model/entities/locations/printer.dart'; import 'package:uni/model/entities/locations/restaurant_location.dart'; import 'package:uni/model/entities/locations/room_group_location.dart'; +import 'package:uni/model/entities/locations/room_location.dart'; import 'package:uni/model/entities/locations/special_room_location.dart'; import 'package:uni/model/entities/locations/store_location.dart'; import 'package:uni/model/entities/locations/unknown_location.dart'; import 'package:uni/model/entities/locations/vending_machine.dart'; import 'package:uni/model/entities/locations/wc_location.dart'; -import 'package:uni/model/entities/locations/atm.dart'; -import 'package:uni/model/entities/locations/printer.dart'; -import 'package:uni/model/entities/locations/room_location.dart'; enum LocationType { vendingMachine, @@ -45,23 +45,24 @@ String locationTypeToString(LocationType type) { return 'STORE'; case LocationType.wc: return 'WC'; - default: - return 'LOCATION'; } } abstract class Location { + // String or IconData + Location(this.floor, this.weight, this.icon); + final int floor; final int weight; - final dynamic icon; // String or IconData - Location(this.floor, this.weight, this.icon, {locationGroupId}); + final dynamic icon; String description(); Map toMap({int? groupId}); + // ignore_for_file: argument_type_not_assignable static Location fromJSON(Map json, int floor) { - final Map args = json['args']; + final args = json['args'] as Map; switch (json['type']) { case 'COFFEE_MACHINE': return CoffeeMachine(floor); diff --git a/uni/lib/model/entities/location_group.dart b/uni/lib/model/entities/location_group.dart index 0d2685474..6ba918095 100644 --- a/uni/lib/model/entities/location_group.dart +++ b/uni/lib/model/entities/location_group.dart @@ -5,22 +5,25 @@ import 'package:uni/model/entities/location.dart'; /// Store information about a location marker. /// What's located in each floor, like vending machines, rooms, etc... class LocationGroup { + LocationGroup( + this.latlng, { + List? locations, + this.isFloorless = false, + this.id, + }) : floors = locations != null + ? groupBy(locations, (location) => location.floor) + : Map.identity(); final Map> floors; final bool isFloorless; final LatLng latlng; final int? id; - LocationGroup(this.latlng, - {List? locations, this.isFloorless = false, this.id}) - : floors = locations != null - ? groupBy(locations, (location) => location.floor) - : Map.identity(); - /// Returns the Location with the most weight Location? getLocationWithMostWeight() { - final List allLocations = floors.values.expand((x) => x).toList(); + final allLocations = floors.values.expand((x) => x).toList(); return allLocations.reduce( - (current, next) => current.weight > next.weight ? current : next); + (current, next) => current.weight > next.weight ? current : next, + ); } Map toMap() { diff --git a/uni/lib/model/entities/locations/atm.dart b/uni/lib/model/entities/locations/atm.dart index 6a0637dc6..ea7634ee6 100644 --- a/uni/lib/model/entities/locations/atm.dart +++ b/uni/lib/model/entities/locations/atm.dart @@ -1,8 +1,8 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class Atm implements Location { + Atm(this.floor, {this.locationGroupId}) : super(); @override final int floor; @@ -14,8 +14,6 @@ class Atm implements Location { final int? locationGroupId; - Atm(this.floor, {this.locationGroupId}) : super(); - @override String description() { return 'Atm'; diff --git a/uni/lib/model/entities/locations/coffee_machine.dart b/uni/lib/model/entities/locations/coffee_machine.dart index 32709c471..acda7feea 100644 --- a/uni/lib/model/entities/locations/coffee_machine.dart +++ b/uni/lib/model/entities/locations/coffee_machine.dart @@ -1,8 +1,8 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class CoffeeMachine implements Location { + CoffeeMachine(this.floor, {this.locationGroupId}); @override final int floor; @@ -14,8 +14,6 @@ class CoffeeMachine implements Location { final int? locationGroupId; - CoffeeMachine(this.floor, {this.locationGroupId}); - @override String description() { return 'Máquina de café'; diff --git a/uni/lib/model/entities/locations/printer.dart b/uni/lib/model/entities/locations/printer.dart index cf2ce7fd3..f269b42ef 100644 --- a/uni/lib/model/entities/locations/printer.dart +++ b/uni/lib/model/entities/locations/printer.dart @@ -1,8 +1,8 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class Printer implements Location { + Printer(this.floor, {this.locationGroupId}); @override final int floor; @@ -14,8 +14,6 @@ class Printer implements Location { final int? locationGroupId; - Printer(this.floor, {this.locationGroupId}); - @override String description() { return 'Impressora'; diff --git a/uni/lib/model/entities/locations/restaurant_location.dart b/uni/lib/model/entities/locations/restaurant_location.dart index 73a7df5e9..022f65f9f 100644 --- a/uni/lib/model/entities/locations/restaurant_location.dart +++ b/uni/lib/model/entities/locations/restaurant_location.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; class RestaurantLocation implements Location { + RestaurantLocation(this.floor, this.name, {this.locationGroupId}); @override final int floor; @@ -15,8 +16,6 @@ class RestaurantLocation implements Location { final int? locationGroupId; - RestaurantLocation(this.floor, this.name, {this.locationGroupId}); - @override String description() { return name; diff --git a/uni/lib/model/entities/locations/room_group_location.dart b/uni/lib/model/entities/locations/room_group_location.dart index 3ceb21347..09b4416f8 100644 --- a/uni/lib/model/entities/locations/room_group_location.dart +++ b/uni/lib/model/entities/locations/room_group_location.dart @@ -1,8 +1,13 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class RoomGroupLocation implements Location { + RoomGroupLocation( + this.floor, + this.firstRoomNumber, + this.secondRoomNumber, { + this.locationGroupId, + }); @override final int floor; @@ -16,9 +21,6 @@ class RoomGroupLocation implements Location { final int? locationGroupId; - RoomGroupLocation(this.floor, this.firstRoomNumber, this.secondRoomNumber, - {this.locationGroupId}); - @override String description() { return '''$firstRoomNumber -> $secondRoomNumber'''; diff --git a/uni/lib/model/entities/locations/room_location.dart b/uni/lib/model/entities/locations/room_location.dart index d8e1836ee..4f719bd7a 100644 --- a/uni/lib/model/entities/locations/room_location.dart +++ b/uni/lib/model/entities/locations/room_location.dart @@ -1,8 +1,8 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class RoomLocation implements Location { + RoomLocation(this.floor, this.roomNumber, {this.locationGroupId}); @override final int floor; @@ -15,8 +15,6 @@ class RoomLocation implements Location { final int? locationGroupId; - RoomLocation(this.floor, this.roomNumber, {this.locationGroupId}); - @override String description() { return roomNumber; diff --git a/uni/lib/model/entities/locations/special_room_location.dart b/uni/lib/model/entities/locations/special_room_location.dart index 31bb1dc53..274b4cf40 100644 --- a/uni/lib/model/entities/locations/special_room_location.dart +++ b/uni/lib/model/entities/locations/special_room_location.dart @@ -3,6 +3,12 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; class SpecialRoomLocation implements Location { + SpecialRoomLocation( + this.floor, + this.roomNumber, + this.name, { + this.locationGroupId, + }); @override final int floor; @@ -16,9 +22,6 @@ class SpecialRoomLocation implements Location { final int? locationGroupId; - SpecialRoomLocation(this.floor, this.roomNumber, this.name, - {this.locationGroupId}); - @override String description() { return '''$roomNumber - $name'''; diff --git a/uni/lib/model/entities/locations/store_location.dart b/uni/lib/model/entities/locations/store_location.dart index 0115c04ee..05817bc5b 100644 --- a/uni/lib/model/entities/locations/store_location.dart +++ b/uni/lib/model/entities/locations/store_location.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; class StoreLocation implements Location { + StoreLocation(this.floor, this.name, {this.locationGroupId}); @override final int floor; @@ -15,8 +16,6 @@ class StoreLocation implements Location { final int? locationGroupId; - StoreLocation(this.floor, this.name, {this.locationGroupId}); - @override String description() { return name; diff --git a/uni/lib/model/entities/locations/unknown_location.dart b/uni/lib/model/entities/locations/unknown_location.dart index fcd1a8cbf..121ab3e4d 100644 --- a/uni/lib/model/entities/locations/unknown_location.dart +++ b/uni/lib/model/entities/locations/unknown_location.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; class UnknownLocation implements Location { + UnknownLocation(this.floor, this.type, {this.locationGroupId}); @override final int floor; @@ -16,8 +17,6 @@ class UnknownLocation implements Location { final String type; - UnknownLocation(this.floor, this.type, {this.locationGroupId}); - @override String description() { return type; diff --git a/uni/lib/model/entities/locations/vending_machine.dart b/uni/lib/model/entities/locations/vending_machine.dart index cd62d2645..f1198a3d8 100644 --- a/uni/lib/model/entities/locations/vending_machine.dart +++ b/uni/lib/model/entities/locations/vending_machine.dart @@ -1,8 +1,8 @@ -import 'package:uni/view/locations/widgets/icons.dart'; - import 'package:uni/model/entities/location.dart'; +import 'package:uni/view/locations/widgets/icons.dart'; class VendingMachine implements Location { + VendingMachine(this.floor, {this.locationGroupId}); @override final int floor; @@ -14,8 +14,6 @@ class VendingMachine implements Location { final int? locationGroupId; - VendingMachine(this.floor, {this.locationGroupId}); - @override String description() { return 'Máquina de venda'; diff --git a/uni/lib/model/entities/locations/wc_location.dart b/uni/lib/model/entities/locations/wc_location.dart index 2e2db38ab..058f025ff 100644 --- a/uni/lib/model/entities/locations/wc_location.dart +++ b/uni/lib/model/entities/locations/wc_location.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/location.dart'; class WcLocation implements Location { + WcLocation(this.floor, {this.locationGroupId}); @override final int floor; @@ -13,8 +14,6 @@ class WcLocation implements Location { final int? locationGroupId; - WcLocation(this.floor, {this.locationGroupId}); - @override String description() { return 'Casa de banho'; diff --git a/uni/lib/model/entities/login_exceptions.dart b/uni/lib/model/entities/login_exceptions.dart index 908a96dd0..c170f6405 100644 --- a/uni/lib/model/entities/login_exceptions.dart +++ b/uni/lib/model/entities/login_exceptions.dart @@ -3,11 +3,11 @@ class ExpiredCredentialsException implements Exception { } class InternetStatusException implements Exception { - String message = 'Verifica a tua ligação à internet'; InternetStatusException(); + String message = 'Verifica a tua ligação à internet'; } class WrongCredentialsException implements Exception { - String message = 'Credenciais inválidas'; WrongCredentialsException(); + String message = 'Credenciais inválidas'; } diff --git a/uni/lib/model/entities/meal.dart b/uni/lib/model/entities/meal.dart index 3491be862..ab278c832 100644 --- a/uni/lib/model/entities/meal.dart +++ b/uni/lib/model/entities/meal.dart @@ -2,14 +2,14 @@ import 'package:intl/intl.dart'; import 'package:uni/model/utils/day_of_week.dart'; class Meal { + Meal(this.type, this.name, this.dayOfWeek, this.date); final String type; final String name; final DayOfWeek dayOfWeek; final DateTime date; - Meal(this.type, this.name, this.dayOfWeek, this.date); - Map toMap(restaurantId) { - final DateFormat format = DateFormat('d-M-y'); + Map toMap(int restaurantId) { + final format = DateFormat('d-M-y'); return { 'id': null, 'day': toString(dayOfWeek), diff --git a/uni/lib/model/entities/profile.dart b/uni/lib/model/entities/profile.dart index be5bdf835..59fecf190 100644 --- a/uni/lib/model/entities/profile.dart +++ b/uni/lib/model/entities/profile.dart @@ -1,42 +1,46 @@ import 'dart:convert'; +import 'package:http/http.dart'; import 'package:tuple/tuple.dart'; import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; /// Stores information about the user's profile. class Profile { - final String name; - final String email; - final String printBalance; - final String feesBalance; - final DateTime? feesLimit; - List courses; - List courseUnits; - - Profile( - {this.name = '', - this.email = '', - courses, - this.printBalance = '', - this.feesBalance = '', - this.feesLimit}) - : courses = courses ?? [], + Profile({ + this.name = '', + this.email = '', + List? courses, + this.printBalance = '', + this.feesBalance = '', + this.feesLimit, + }) : courses = courses ?? [], courseUnits = []; /// Creates a new instance from a JSON object. - static Profile fromResponse(dynamic response) { - final responseBody = json.decode(response.body); - final List courses = []; - for (var c in responseBody['cursos']) { - courses.add(Course.fromJson(c)); + factory Profile.fromResponse(Response response) { + var responseBody = json.decode(response.body); + responseBody = responseBody as Map; + final courses = []; + for (final c in responseBody['cursos'] as List) { + courses.add(Course.fromJson(c as Map)); } + return Profile( - name: responseBody['nome'], - email: responseBody['email'], - courses: courses); + name: responseBody['nome'] as String? ?? '', + email: responseBody['email'] as String? ?? '', + courses: courses, + ); } + final String name; + final String email; + String printBalance; + String feesBalance; + DateTime? feesLimit; + List courses; + List courseUnits; + /// Returns a list with two tuples: the first tuple contains the user's name /// and the other one contains the user's email. List> keymapValues() { diff --git a/uni/lib/model/entities/reference.dart b/uni/lib/model/entities/reference.dart index 63a249845..3c038ead3 100644 --- a/uni/lib/model/entities/reference.dart +++ b/uni/lib/model/entities/reference.dart @@ -1,13 +1,17 @@ class Reference { + Reference( + this.description, + this.limitDate, + this.entity, + this.reference, + this.amount, + ); final String description; final DateTime limitDate; final int entity; final int reference; final double amount; - Reference(this.description, this.limitDate, this.entity, this.reference, - this.amount); - /// Converts this reference to a map. Map toMap() { return { diff --git a/uni/lib/model/entities/restaurant.dart b/uni/lib/model/entities/restaurant.dart index 420444a26..a48786314 100644 --- a/uni/lib/model/entities/restaurant.dart +++ b/uni/lib/model/entities/restaurant.dart @@ -3,22 +3,26 @@ import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/utils/day_of_week.dart'; class Restaurant { + Restaurant(this.id, this.name, this.reference, {required List meals}) + : meals = groupBy(meals, (meal) => meal.dayOfWeek); + + factory Restaurant.fromMap(Map map, List meals) { + return Restaurant( + map['id'] as int?, + map['name'] as String, + map['ref'] as String, + meals: meals, + ); + } final int? id; final String name; final String reference; // Used only in html parser final Map> meals; - get isNotEmpty { + bool get isNotEmpty { return meals.isNotEmpty; } - Restaurant(this.id, this.name, this.reference, {required List meals}) - : meals = groupBy(meals, (meal) => meal.dayOfWeek); - - static Restaurant fromMap(Map map, List meals) { - return Restaurant(map['id'], map['name'], map['ref'], meals: meals); - } - List getMealsOfDay(DayOfWeek dayOfWeek) { return meals[dayOfWeek] ?? []; } diff --git a/uni/lib/model/entities/session.dart b/uni/lib/model/entities/session.dart index 7108ddd89..636c65a8b 100644 --- a/uni/lib/model/entities/session.dart +++ b/uni/lib/model/entities/session.dart @@ -1,37 +1,39 @@ import 'dart:convert'; -import 'package:uni/controller/networking/network_router.dart'; import 'package:http/http.dart' as http; +import 'package:uni/controller/networking/network_router.dart'; /// Stores information about a user session. class Session { + Session({ + required this.username, + required this.cookies, + required this.faculties, + this.persistentSession = false, + }) : assert(faculties.isNotEmpty, 'session must have faculties'); + String username; String cookies; List faculties; bool persistentSession; - Session( - {required this.username, - required this.cookies, - required this.faculties, - this.persistentSession = false}) { - assert(faculties.isNotEmpty); - } - /// Creates a new Session instance from an HTTP response. /// Returns null if the authentication failed. static Session? fromLogin( - http.Response response, List faculties, bool persistentSession) { - final responseBody = json.decode(response.body); + http.Response response, + List faculties, { + required bool persistentSession, + }) { + final responseBody = json.decode(response.body) as Map; - if (!responseBody['authenticated']) { + if (!(responseBody['authenticated'] as bool)) { return null; } return Session( - faculties: faculties, - username: responseBody['codigo'], - cookies: NetworkRouter.extractCookies(response.headers), - persistentSession: false); + faculties: faculties, + username: responseBody['codigo'] as String, + cookies: NetworkRouter.extractCookies(response.headers), + ); } } diff --git a/uni/lib/model/entities/time_utilities.dart b/uni/lib/model/entities/time_utilities.dart index a34dc561d..cd62196d2 100644 --- a/uni/lib/model/entities/time_utilities.dart +++ b/uni/lib/model/entities/time_utilities.dart @@ -2,12 +2,15 @@ import 'package:flutter/material.dart'; extension TimeString on DateTime { String toTimeHourMinString() { - return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; + return '${hour.toString().padLeft(2, '0')}:' + '${minute.toString().padLeft(2, '0')}'; } - static List getWeekdaysStrings( - {bool startMonday = true, bool includeWeekend = true}) { - final List weekdays = [ + static List getWeekdaysStrings({ + bool startMonday = true, + bool includeWeekend = true, + }) { + final weekdays = [ 'Segunda-Feira', 'Terça-Feira', 'Quarta-Feira', @@ -18,8 +21,9 @@ extension TimeString on DateTime { ]; if (!startMonday) { - weekdays.removeAt(6); - weekdays.insert(0, 'Domingo'); + weekdays + ..removeAt(6) + ..insert(0, 'Domingo'); } return includeWeekend ? weekdays : weekdays.sublist(0, 5); @@ -28,7 +32,7 @@ extension TimeString on DateTime { extension ClosestMonday on DateTime { DateTime getClosestMonday() { - final DateTime day = DateUtils.dateOnly(this); + final day = DateUtils.dateOnly(this); if (day.weekday >= 1 && day.weekday <= 5) { return day.subtract(Duration(days: day.weekday - 1)); } diff --git a/uni/lib/model/entities/trip.dart b/uni/lib/model/entities/trip.dart index 77180fb6b..73b4ce6b4 100644 --- a/uni/lib/model/entities/trip.dart +++ b/uni/lib/model/entities/trip.dart @@ -2,15 +2,15 @@ import 'package:logger/logger.dart'; /// Stores information about a bus trip. class Trip { + Trip({ + required this.line, + required this.destination, + required this.timeRemaining, + }); final String line; final String destination; final int timeRemaining; - Trip( - {required this.line, - required this.destination, - required this.timeRemaining}); - /// Converts this trip to a map. Map toMap() { return { @@ -27,6 +27,6 @@ class Trip { /// Compares the remaining time of two trips. int compare(Trip other) { - return (timeRemaining.compareTo(other.timeRemaining)); + return timeRemaining.compareTo(other.timeRemaining); } } diff --git a/uni/lib/model/providers/lazy/bus_stop_provider.dart b/uni/lib/model/providers/lazy/bus_stop_provider.dart index 50e478295..1c3b81a61 100644 --- a/uni/lib/model/providers/lazy/bus_stop_provider.dart +++ b/uni/lib/model/providers/lazy/bus_stop_provider.dart @@ -7,16 +7,14 @@ import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/entities/trip.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class BusStopProvider extends StateProviderNotifier { + BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); Map _configuredBusStops = Map.identity(); DateTime _timeStamp = DateTime.now(); - BusStopProvider() : super(dependsOnSession: false, cacheDuration: null); - UnmodifiableMapView get configuredBusStops => UnmodifiableMapView(_configuredBusStops); @@ -24,8 +22,8 @@ class BusStopProvider extends StateProviderNotifier { @override Future loadFromStorage() async { - final AppBusStopDatabase busStopsDb = AppBusStopDatabase(); - final Map stops = await busStopsDb.busStops(); + final busStopsDb = AppBusStopDatabase(); + final stops = await busStopsDb.busStops(); _configuredBusStops = stops; } @@ -36,10 +34,11 @@ class BusStopProvider extends StateProviderNotifier { Future fetchUserBusTrips() async { try { - for (String stopCode in configuredBusStops.keys) { - final List stopTrips = - await DeparturesFetcher.getNextArrivalsStop( - stopCode, configuredBusStops[stopCode]!); + for (final stopCode in configuredBusStops.keys) { + final stopTrips = await DeparturesFetcher.getNextArrivalsStop( + stopCode, + configuredBusStops[stopCode]!, + ); _configuredBusStops[stopCode]?.trips = stopTrips; } _timeStamp = DateTime.now(); @@ -51,8 +50,6 @@ class BusStopProvider extends StateProviderNotifier { } Future addUserBusStop(String stopCode, BusStopData stopData) async { - updateStatus(RequestStatus.busy); - if (_configuredBusStops.containsKey(stopCode)) { (_configuredBusStops[stopCode]!.configuredBuses).clear(); _configuredBusStops[stopCode]! @@ -62,32 +59,37 @@ class BusStopProvider extends StateProviderNotifier { _configuredBusStops[stopCode] = stopData; } + updateStatus(RequestStatus.busy); await fetchUserBusTrips(); - final AppBusStopDatabase db = AppBusStopDatabase(); - db.setBusStops(configuredBusStops); + final db = AppBusStopDatabase(); + await db.setBusStops(configuredBusStops); } - Future removeUserBusStop(String stopCode) async { + Future removeUserBusStop( + String stopCode, + ) async { updateStatus(RequestStatus.busy); _configuredBusStops.remove(stopCode); notifyListeners(); await fetchUserBusTrips(); - final AppBusStopDatabase db = AppBusStopDatabase(); - db.setBusStops(_configuredBusStops); + final db = AppBusStopDatabase(); + await db.setBusStops(_configuredBusStops); } Future toggleFavoriteUserBusStop( - String stopCode, BusStopData stopData) async { + String stopCode, + BusStopData stopData, + ) async { _configuredBusStops[stopCode]!.favorited = !_configuredBusStops[stopCode]!.favorited; notifyListeners(); await fetchUserBusTrips(); - final AppBusStopDatabase db = AppBusStopDatabase(); - db.updateFavoriteBusStop(stopCode); + final db = AppBusStopDatabase(); + await db.updateFavoriteBusStop(stopCode); } } diff --git a/uni/lib/model/providers/lazy/calendar_provider.dart b/uni/lib/model/providers/lazy/calendar_provider.dart index 4776f6bcf..5be749683 100644 --- a/uni/lib/model/providers/lazy/calendar_provider.dart +++ b/uni/lib/model/providers/lazy/calendar_provider.dart @@ -10,10 +10,9 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class CalendarProvider extends StateProviderNotifier { - List _calendar = []; - CalendarProvider() : super(dependsOnSession: true, cacheDuration: const Duration(days: 30)); + List _calendar = []; UnmodifiableListView get calendar => UnmodifiableListView(_calendar); @@ -27,8 +26,8 @@ class CalendarProvider extends StateProviderNotifier { try { _calendar = await CalendarFetcherHtml().getCalendar(session); - final CalendarDatabase db = CalendarDatabase(); - db.saveCalendar(calendar); + final db = CalendarDatabase(); + unawaited(db.saveCalendar(calendar)); updateStatus(RequestStatus.successful); } catch (e) { @@ -38,7 +37,7 @@ class CalendarProvider extends StateProviderNotifier { @override Future loadFromStorage() async { - final CalendarDatabase db = CalendarDatabase(); + final db = CalendarDatabase(); _calendar = await db.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 8b3851264..1f1e76a0b 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -11,11 +11,10 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class CourseUnitsInfoProvider extends StateProviderNotifier { - final Map _courseUnitsSheets = {}; - final Map> _courseUnitsClasses = {}; - CourseUnitsInfoProvider() : super(dependsOnSession: true, cacheDuration: null, initialize: false); + final Map _courseUnitsSheets = {}; + final Map> _courseUnitsClasses = {}; UnmodifiableMapView get courseUnitsSheets => UnmodifiableMapView(_courseUnitsSheets); @@ -23,7 +22,10 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { UnmodifiableMapView> get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); - fetchCourseUnitSheet(CourseUnit courseUnit, Session session) async { + Future fetchCourseUnitSheet( + CourseUnit courseUnit, + Session session, + ) async { updateStatus(RequestStatus.busy); try { _courseUnitsSheets[courseUnit] = await CourseUnitsInfoFetcher() @@ -36,7 +38,10 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } - fetchCourseUnitClasses(CourseUnit courseUnit, Session session) async { + Future fetchCourseUnitClasses( + CourseUnit courseUnit, + Session session, + ) async { updateStatus(RequestStatus.busy); try { _courseUnitsClasses[courseUnit] = await CourseUnitsInfoFetcher() diff --git a/uni/lib/model/providers/lazy/exam_provider.dart b/uni/lib/model/providers/lazy/exam_provider.dart index 161573f08..562424f80 100644 --- a/uni/lib/model/providers/lazy/exam_provider.dart +++ b/uni/lib/model/providers/lazy/exam_provider.dart @@ -14,13 +14,12 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class ExamProvider extends StateProviderNotifier { + ExamProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); List _exams = []; List _hiddenExams = []; Map _filteredExamsTypes = {}; - ExamProvider() - : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); - UnmodifiableListView get exams => UnmodifiableListView(_exams); UnmodifiableListView get hiddenExams => @@ -34,19 +33,20 @@ class ExamProvider extends StateProviderNotifier { await setFilteredExams(await AppSharedPreferences.getFilteredExams()); await setHiddenExams(await AppSharedPreferences.getHiddenExams()); - final AppExamsDatabase db = AppExamsDatabase(); - final List exams = await db.exams(); + final db = AppExamsDatabase(); + final exams = await db.exams(); _exams = exams; } @override Future loadFromRemote(Session session, Profile profile) async { await fetchUserExams( - ParserExams(), - await AppSharedPreferences.getPersistentUserInfo(), - profile, - session, - profile.courseUnits); + ParserExams(), + await AppSharedPreferences.getPersistentUserInfo(), + profile, + session, + profile.courseUnits, + ); } Future fetchUserExams( @@ -57,14 +57,13 @@ class ExamProvider extends StateProviderNotifier { List userUcs, ) async { try { - final List exams = await ExamFetcher(profile.courses, userUcs) + final exams = await ExamFetcher(profile.courses, userUcs) .extractExams(session, parserExams); exams.sort((exam1, exam2) => exam1.begin.compareTo(exam2.begin)); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { - final AppExamsDatabase db = AppExamsDatabase(); - db.saveNewExams(exams); + await AppExamsDatabase().saveNewExams(exams); } _exams = exams; @@ -74,26 +73,27 @@ class ExamProvider extends StateProviderNotifier { } } - updateFilteredExams() async { + Future updateFilteredExams() async { final exams = await AppSharedPreferences.getFilteredExams(); _filteredExamsTypes = exams; notifyListeners(); } Future setFilteredExams(Map newFilteredExams) async { - AppSharedPreferences.saveFilteredExams(filteredExamsTypes); + unawaited(AppSharedPreferences.saveFilteredExams(filteredExamsTypes)); _filteredExamsTypes = Map.from(newFilteredExams); notifyListeners(); } List getFilteredExams() { return exams - .where((exam) => - filteredExamsTypes[Exam.getExamTypeLong(exam.type)] ?? true) + .where( + (exam) => filteredExamsTypes[Exam.getExamTypeLong(exam.type)] ?? true, + ) .toList(); } - setHiddenExams(List newHiddenExams) async { + Future setHiddenExams(List newHiddenExams) async { _hiddenExams = List.from(newHiddenExams); await AppSharedPreferences.saveHiddenExams(hiddenExams); notifyListeners(); @@ -107,7 +107,7 @@ class ExamProvider extends StateProviderNotifier { notifyListeners(); } - setExams(List newExams) { + set exams(List newExams) { _exams = newExams; notifyListeners(); } diff --git a/uni/lib/model/providers/lazy/faculty_locations_provider.dart b/uni/lib/model/providers/lazy/faculty_locations_provider.dart index 3f210e468..2952f0e57 100644 --- a/uni/lib/model/providers/lazy/faculty_locations_provider.dart +++ b/uni/lib/model/providers/lazy/faculty_locations_provider.dart @@ -8,10 +8,9 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class FacultyLocationsProvider extends StateProviderNotifier { - List _locations = []; - FacultyLocationsProvider() : super(dependsOnSession: false, cacheDuration: const Duration(days: 30)); + List _locations = []; UnmodifiableListView get locations => UnmodifiableListView(_locations); diff --git a/uni/lib/model/providers/lazy/home_page_provider.dart b/uni/lib/model/providers/lazy/home_page_provider.dart index 31cfa6130..891495761 100644 --- a/uni/lib/model/providers/lazy/home_page_provider.dart +++ b/uni/lib/model/providers/lazy/home_page_provider.dart @@ -6,11 +6,10 @@ import 'package:uni/model/request_status.dart'; import 'package:uni/utils/favorite_widget_type.dart'; class HomePageProvider extends StateProviderNotifier { + HomePageProvider() : super(dependsOnSession: false, cacheDuration: null); List _favoriteCards = []; bool _isEditing = false; - HomePageProvider() : super(dependsOnSession: false, cacheDuration: null); - List get favoriteCards => _favoriteCards.toList(); bool get isEditing => _isEditing; @@ -25,17 +24,17 @@ class HomePageProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } - setHomePageEditingMode(bool state) { - _isEditing = state; + void setHomePageEditingMode({required bool editingMode}) { + _isEditing = editingMode; notifyListeners(); } - toggleHomePageEditingMode() { + void toggleHomePageEditingMode() { _isEditing = !_isEditing; notifyListeners(); } - setFavoriteCards(List favoriteCards) { + 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 d8fa13a1e..101e70022 100644 --- a/uni/lib/model/providers/lazy/lecture_provider.dart +++ b/uni/lib/model/providers/lazy/lecture_provider.dart @@ -14,36 +14,41 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class LectureProvider extends StateProviderNotifier { - List _lectures = []; - LectureProvider() : super(dependsOnSession: true, cacheDuration: const Duration(hours: 6)); + List _lectures = []; UnmodifiableListView get lectures => UnmodifiableListView(_lectures); @override Future loadFromStorage() async { - final AppLecturesDatabase db = AppLecturesDatabase(); - final List lectures = await db.lectures(); + final db = AppLecturesDatabase(); + final lectures = await db.lectures(); _lectures = lectures; } @override Future loadFromRemote(Session session, Profile profile) async { await fetchUserLectures( - await AppSharedPreferences.getPersistentUserInfo(), session, profile); + await AppSharedPreferences.getPersistentUserInfo(), + session, + profile, + ); } - Future fetchUserLectures(Tuple2 userPersistentInfo, - Session session, Profile profile, - {ScheduleFetcher? fetcher}) async { + Future fetchUserLectures( + Tuple2 userPersistentInfo, + Session session, + Profile profile, { + ScheduleFetcher? fetcher, + }) async { try { - final List lectures = + final lectures = await getLecturesFromFetcherOrElse(fetcher, session, profile); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { - final AppLecturesDatabase db = AppLecturesDatabase(); - db.saveNewLectures(lectures); + final db = AppLecturesDatabase(); + await db.saveNewLectures(lectures); } _lectures = lectures; @@ -54,7 +59,10 @@ class LectureProvider extends StateProviderNotifier { } Future> getLecturesFromFetcherOrElse( - ScheduleFetcher? fetcher, Session session, Profile profile) => + ScheduleFetcher? fetcher, + Session session, + Profile profile, + ) => (fetcher?.getLectures(session, profile)) ?? getLectures(session, profile); Future> getLectures(Session session, Profile profile) { diff --git a/uni/lib/model/providers/lazy/library_occupation_provider.dart b/uni/lib/model/providers/lazy/library_occupation_provider.dart index 26a1dbeb5..ee1ab2292 100644 --- a/uni/lib/model/providers/lazy/library_occupation_provider.dart +++ b/uni/lib/model/providers/lazy/library_occupation_provider.dart @@ -9,17 +9,16 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class LibraryOccupationProvider extends StateProviderNotifier { - LibraryOccupation? _occupation; - LibraryOccupationProvider() : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); + LibraryOccupation? _occupation; LibraryOccupation? get occupation => _occupation; @override Future loadFromStorage() async { - final LibraryOccupationDatabase db = LibraryOccupationDatabase(); - final LibraryOccupation occupation = await db.occupation(); + final db = LibraryOccupationDatabase(); + final occupation = await db.occupation(); _occupation = occupation; } @@ -33,8 +32,8 @@ class LibraryOccupationProvider extends StateProviderNotifier { _occupation = await LibraryOccupationFetcherSheets() .getLibraryOccupationFromSheets(session); - final LibraryOccupationDatabase db = LibraryOccupationDatabase(); - db.saveOccupation(_occupation!); + final db = LibraryOccupationDatabase(); + unawaited(db.saveOccupation(_occupation!)); updateStatus(RequestStatus.successful); } catch (e) { diff --git a/uni/lib/model/providers/lazy/reference_provider.dart b/uni/lib/model/providers/lazy/reference_provider.dart index 829658416..d795e070f 100644 --- a/uni/lib/model/providers/lazy/reference_provider.dart +++ b/uni/lib/model/providers/lazy/reference_provider.dart @@ -11,18 +11,17 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class ReferenceProvider extends StateProviderNotifier { - List _references = []; - ReferenceProvider() : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); + List _references = []; UnmodifiableListView get references => UnmodifiableListView(_references); @override Future loadFromStorage() async { - final AppReferencesDatabase referencesDb = AppReferencesDatabase(); - final List references = await referencesDb.references(); + final referencesDb = AppReferencesDatabase(); + final references = await referencesDb.references(); _references = references; } @@ -41,7 +40,7 @@ class ReferenceProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); final referencesDb = AppReferencesDatabase(); - referencesDb.saveNewReferences(references); + unawaited(referencesDb.saveNewReferences(references)); } catch (e) { updateStatus(RequestStatus.failed); } diff --git a/uni/lib/model/providers/lazy/restaurant_provider.dart b/uni/lib/model/providers/lazy/restaurant_provider.dart index 9634de134..e6eb0fea8 100644 --- a/uni/lib/model/providers/lazy/restaurant_provider.dart +++ b/uni/lib/model/providers/lazy/restaurant_provider.dart @@ -10,18 +10,17 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class RestaurantProvider extends StateProviderNotifier { - List _restaurants = []; - RestaurantProvider() : super(dependsOnSession: false, cacheDuration: const Duration(days: 1)); + List _restaurants = []; UnmodifiableListView get restaurants => UnmodifiableListView(_restaurants); @override Future loadFromStorage() async { - final RestaurantDatabase restaurantDb = RestaurantDatabase(); - final List restaurants = await restaurantDb.getRestaurants(); + final restaurantDb = RestaurantDatabase(); + final restaurants = await restaurantDb.getRestaurants(); _restaurants = restaurants; } @@ -32,11 +31,10 @@ class RestaurantProvider extends StateProviderNotifier { Future fetchRestaurants(Session session) async { try { - final List restaurants = - await RestaurantFetcher().getRestaurants(session); + final restaurants = await RestaurantFetcher().getRestaurants(session); - final RestaurantDatabase db = RestaurantDatabase(); - db.saveRestaurants(restaurants); + final db = RestaurantDatabase(); + unawaited(db.saveRestaurants(restaurants)); _restaurants = filterPastMeals(restaurants); diff --git a/uni/lib/model/providers/startup/profile_provider.dart b/uni/lib/model/providers/startup/profile_provider.dart index f28acbb93..ea99c9965 100644 --- a/uni/lib/model/providers/startup/profile_provider.dart +++ b/uni/lib/model/providers/startup/profile_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package: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'; @@ -9,30 +8,20 @@ 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_refresh_times_database.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/controller/local_storage/app_user_database.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/controller/parsers/parser_fees.dart'; import 'package:uni/controller/parsers/parser_print_balance.dart'; -import 'package:uni/model/entities/course.dart'; -import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class ProfileProvider extends StateProviderNotifier { - Profile _profile = Profile(); - DateTime? _feesRefreshTime; - DateTime? _printRefreshTime; - ProfileProvider() : super(dependsOnSession: true, cacheDuration: const Duration(days: 1)); - - String get feesRefreshTime => _feesRefreshTime.toString(); - - String get printRefreshTime => _printRefreshTime.toString(); + Profile _profile = Profile(); Profile get profile => _profile; @@ -40,7 +29,8 @@ class ProfileProvider extends StateProviderNotifier { Future loadFromStorage() async { await loadProfile(); await Future.wait( - [loadCourses(), loadBalanceRefreshTimes(), loadCourseUnits()]); + [loadCourses(), loadCourseUnits()], + ); } @override @@ -64,28 +54,13 @@ class ProfileProvider extends StateProviderNotifier { } Future loadCourses() async { - final AppCoursesDatabase coursesDb = AppCoursesDatabase(); - final List courses = await coursesDb.courses(); + final coursesDb = AppCoursesDatabase(); + final courses = await coursesDb.courses(); _profile.courses = courses; } - Future loadBalanceRefreshTimes() async { - final AppRefreshTimesDatabase refreshTimesDb = AppRefreshTimesDatabase(); - final Map refreshTimes = - await refreshTimesDb.refreshTimes(); - - final printRefreshTime = refreshTimes['print']; - final feesRefreshTime = refreshTimes['fees']; - if (printRefreshTime != null) { - _printRefreshTime = DateTime.parse(printRefreshTime); - } - if (feesRefreshTime != null) { - _feesRefreshTime = DateTime.parse(feesRefreshTime); - } - } - Future loadCourseUnits() async { - final AppCourseUnitsDatabase db = AppCourseUnitsDatabase(); + final db = AppCourseUnitsDatabase(); profile.courseUnits = await db.courseUnits(); } @@ -93,63 +68,38 @@ class ProfileProvider extends StateProviderNotifier { try { final response = await FeesFetcher().getUserFeesResponse(session); - final String feesBalance = await parseFeesBalance(response); - final DateTime? feesLimit = await parseFeesNextLimit(response); + final feesBalance = parseFeesBalance(response); + final feesLimit = parseFeesNextLimit(response); - final DateTime currentTime = DateTime.now(); - final Tuple2 userPersistentInfo = + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); + if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { - await storeRefreshTime('fees', currentTime.toString()); final profileDb = AppUserDataDatabase(); - profileDb.saveUserFees(feesBalance, feesLimit); + await profileDb.saveUserFees(feesBalance, feesLimit); } - final Profile newProfile = Profile( - name: _profile.name, - email: _profile.email, - courses: _profile.courses, - printBalance: _profile.printBalance, - feesBalance: feesBalance, - feesLimit: feesLimit); - - _profile = newProfile; - _feesRefreshTime = currentTime; + _profile + ..feesBalance = feesBalance + ..feesLimit = feesLimit; } catch (e) { updateStatus(RequestStatus.failed); } } - Future storeRefreshTime(String db, String currentTime) async { - final AppRefreshTimesDatabase refreshTimesDatabase = - AppRefreshTimesDatabase(); - refreshTimesDatabase.saveRefreshTime(db, currentTime); - } - Future fetchUserPrintBalance(Session session) async { try { final response = await PrintFetcher().getUserPrintsResponse(session); - final String printBalance = await getPrintsBalance(response); + final printBalance = await getPrintsBalance(response); - final DateTime currentTime = DateTime.now(); - final Tuple2 userPersistentInfo = + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { - await storeRefreshTime('print', currentTime.toString()); final profileDb = AppUserDataDatabase(); - profileDb.saveUserPrintBalance(printBalance); + await profileDb.saveUserPrintBalance(printBalance); } - final Profile newProfile = Profile( - name: _profile.name, - email: _profile.email, - courses: _profile.courses, - printBalance: printBalance, - feesBalance: _profile.feesBalance, - feesLimit: _profile.feesLimit); - - _profile = newProfile; - _printRefreshTime = currentTime; + _profile.printBalance = printBalance; } catch (e) { updateStatus(RequestStatus.failed); } @@ -157,20 +107,21 @@ class ProfileProvider extends StateProviderNotifier { Future fetchUserInfo(Session session) async { try { - final profile = await ProfileFetcher.getProfile(session); + final profile = await ProfileFetcher.fetchProfile(session); final currentCourseUnits = await CurrentCourseUnitsFetcher().getCurrentCourseUnits(session); - _profile = profile; + _profile = profile ?? Profile(); _profile.courseUnits = currentCourseUnits; updateStatus(RequestStatus.successful); - final Tuple2 userPersistentInfo = + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { + // Course units are saved later, so we don't it here final profileDb = AppUserDataDatabase(); - profileDb.insertUserData(_profile); + await profileDb.insertUserData(_profile); } } catch (e) { updateStatus(RequestStatus.failed); @@ -179,16 +130,21 @@ class ProfileProvider extends StateProviderNotifier { Future fetchCourseUnitsAndCourseAverages(Session session) async { try { - final List courses = profile.courses; - final List allCourseUnits = await AllCourseUnitsFetcher() + final courses = profile.courses; + final allCourseUnits = await AllCourseUnitsFetcher() .getAllCourseUnitsAndCourseAverages(profile.courses, session); - _profile.courseUnits = allCourseUnits; + if (allCourseUnits != null) { + _profile.courseUnits = allCourseUnits; + } else { + // Current course units should already have been fetched, + // so this is not a fatal error + } - final Tuple2 userPersistentInfo = + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); if (userPersistentInfo.item1 != '' && userPersistentInfo.item2 != '') { - final AppCoursesDatabase coursesDb = AppCoursesDatabase(); + final coursesDb = AppCoursesDatabase(); await coursesDb.saveNewCourses(courses); final courseUnitsDatabase = AppCourseUnitsDatabase(); @@ -200,17 +156,22 @@ class ProfileProvider extends StateProviderNotifier { } static Future fetchOrGetCachedProfilePicture( - int? studentNumber, Session session, - {forceRetrieval = false}) { - studentNumber ??= int.parse(session.username.replaceAll("up", "")); - - final String faculty = session.faculties[0]; - final String url = + Session session, { + bool forceRetrieval = false, + int? studentNumber, + }) { + studentNumber ??= int.parse(session.username.replaceAll('up', '')); + + final faculty = session.faculties[0]; + final url = 'https://sigarra.up.pt/$faculty/pt/fotografias_service.foto?pct_cod=$studentNumber'; - final Map headers = {}; + final headers = {}; headers['cookie'] = session.cookies; return loadFileFromStorageOrRetrieveNew( - '${studentNumber}_profile_picture', url, headers, - forceRetrieval: forceRetrieval); + '${studentNumber}_profile_picture', + url, + headers, + forceRetrieval: forceRetrieval, + ); } } diff --git a/uni/lib/model/providers/startup/session_provider.dart b/uni/lib/model/providers/startup/session_provider.dart index 96644a1be..b56255723 100644 --- a/uni/lib/model/providers/startup/session_provider.dart +++ b/uni/lib/model/providers/startup/session_provider.dart @@ -12,13 +12,14 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; import 'package:uni/model/request_status.dart'; class SessionProvider extends StateProviderNotifier { - late Session _session; - SessionProvider() : super( - dependsOnSession: false, - cacheDuration: null, - initialStatus: RequestStatus.none); + dependsOnSession: false, + cacheDuration: null, + initialStatus: RequestStatus.none, + ); + + late Session _session; Session get session => _session; @@ -31,29 +32,41 @@ class SessionProvider extends StateProviderNotifier { } void restoreSession( - String username, String password, List faculties) { + String username, + String password, + List faculties, + ) { _session = Session( - faculties: faculties, - username: username, - cookies: "", - persistentSession: true); + faculties: faculties, + username: username, + cookies: '', + persistentSession: true, + ); } - Future postAuthentication(String username, String password, - List faculties, persistentSession) async { + Future postAuthentication( + String username, + String password, + List faculties, { + required bool persistentSession, + }) async { updateStatus(RequestStatus.busy); Session? session; try { session = await NetworkRouter.login( - username, password, faculties, persistentSession); + username, + password, + faculties, + persistentSession: persistentSession, + ); } catch (e) { updateStatus(RequestStatus.failed); throw InternetStatusException(); } if (session == null) { - final String responseHtml = + final responseHtml = await NetworkRouter.loginInSigarra(username, password, faculties); updateStatus(RequestStatus.failed); @@ -69,11 +82,16 @@ class SessionProvider extends StateProviderNotifier { if (persistentSession) { await AppSharedPreferences.savePersistentUserInfo( - username, password, faculties); + username, + password, + faculties, + ); } - Future.delayed(const Duration(seconds: 20), - () => {NotificationManager().initializeNotifications()}); + Future.delayed( + const Duration(seconds: 20), + () => {NotificationManager().initializeNotifications()}, + ); await acceptTermsAndConditions(); updateStatus(RequestStatus.successful); diff --git a/uni/lib/model/providers/state_provider_notifier.dart b/uni/lib/model/providers/state_provider_notifier.dart index 5d8acc1b7..e74ef2259 100644 --- a/uni/lib/model/providers/state_provider_notifier.dart +++ b/uni/lib/model/providers/state_provider_notifier.dart @@ -11,7 +11,18 @@ import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/model/request_status.dart'; abstract class StateProviderNotifier extends ChangeNotifier { - static final Lock _lock = Lock(); + StateProviderNotifier({ + required this.dependsOnSession, + required this.cacheDuration, + RequestStatus initialStatus = RequestStatus.busy, + bool initialize = true, + }) : _initialStatus = initialStatus, + _status = initialStatus, + _initializedFromStorage = !initialize, + _initializedFromRemote = !initialize; + + final Lock _lock = Lock(); + final RequestStatus _initialStatus; RequestStatus _status; bool _initializedFromStorage; bool _initializedFromRemote; @@ -23,44 +34,45 @@ abstract class StateProviderNotifier extends ChangeNotifier { DateTime? get lastUpdateTime => _lastUpdateTime; - StateProviderNotifier( - {required this.dependsOnSession, - required this.cacheDuration, - RequestStatus initialStatus = RequestStatus.busy, - bool initialize = true}) - : _status = initialStatus, - _initializedFromStorage = !initialize, - _initializedFromRemote = !initialize; + void markAsNotInitialized() { + _initializedFromStorage = false; + _initializedFromRemote = false; + _status = _initialStatus; + _lastUpdateTime = null; + } Future _loadFromStorage() async { _lastUpdateTime = await AppSharedPreferences.getLastDataClassUpdateTime( - runtimeType.toString()); + runtimeType.toString(), + ); await loadFromStorage(); notifyListeners(); - Logger().i("Loaded $runtimeType info from storage"); + Logger().i('Loaded $runtimeType info from storage'); } - Future _loadFromRemote(Session session, Profile profile, - {bool force = false}) async { + Future _loadFromRemote( + Session session, + Profile profile, { + bool force = false, + }) async { final shouldReload = force || _lastUpdateTime == null || cacheDuration == null || DateTime.now().difference(_lastUpdateTime!) > cacheDuration!; if (!shouldReload) { - Logger().i( - "Last info for $runtimeType is within cache period (last updated on $_lastUpdateTime); " - "skipping remote load"); + Logger().i('Last info for $runtimeType is within cache period ' + '(last updated on $_lastUpdateTime); skipping remote load'); updateStatus(RequestStatus.successful); return; } - final bool hasConnectivity = + final hasConnectivity = await Connectivity().checkConnectivity() != ConnectivityResult.none; if (!hasConnectivity) { - Logger().w("No internet connection; skipping $runtimeType remote load"); + Logger().w('No internet connection; skipping $runtimeType remote load'); updateStatus(RequestStatus.successful); return; } @@ -70,16 +82,18 @@ abstract class StateProviderNotifier extends ChangeNotifier { await loadFromRemote(session, profile); if (_status == RequestStatus.successful) { - Logger().i("Loaded $runtimeType info from remote"); + Logger().i('Loaded $runtimeType info from remote'); _lastUpdateTime = DateTime.now(); notifyListeners(); await AppSharedPreferences.setLastDataClassUpdateTime( - runtimeType.toString(), _lastUpdateTime!); + runtimeType.toString(), + _lastUpdateTime!, + ); } else if (_status == RequestStatus.failed) { - Logger().e("Failed to load $runtimeType info from remote"); + Logger().e('Failed to load $runtimeType info from remote'); } else { Logger() - .w("$runtimeType remote load method did not update request status"); + .w('$runtimeType remote load method did not update request status'); } } @@ -94,7 +108,9 @@ abstract class StateProviderNotifier extends ChangeNotifier { DateTime.now().difference(_lastUpdateTime!) < const Duration(minutes: 1)) { Logger().w( - "Last update for $runtimeType was less than a minute ago; skipping refresh"); + 'Last update for $runtimeType was less than a minute ago; ' + 'skipping refresh', + ); return; } @@ -103,7 +119,7 @@ abstract class StateProviderNotifier extends ChangeNotifier { final profile = Provider.of(context, listen: false).profile; - _loadFromRemote(session, profile, force: true); + await _loadFromRemote(session, profile, force: true); }); } @@ -152,7 +168,8 @@ abstract class StateProviderNotifier extends ChangeNotifier { /// This will run once when the provider is first initialized. /// If the data is not available from the remote server /// or the data is filled into the provider on demand, - /// this method should simply set the request status to [RequestStatus.successful]; + /// this method should simply set the + /// request status to [RequestStatus.successful]; /// otherwise, it should set the status accordingly. Future loadFromRemote(Session session, Profile profile); } diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index b464a4e9b..d0d6dc5cb 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -14,34 +14,22 @@ import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; class StateProviders { - final LectureProvider lectureProvider; - final ExamProvider examProvider; - final BusStopProvider busStopProvider; - final RestaurantProvider restaurantProvider; - final CourseUnitsInfoProvider courseUnitsInfoProvider; - final ProfileProvider profileProvider; - final SessionProvider sessionProvider; - final CalendarProvider calendarProvider; - final LibraryOccupationProvider libraryOccupationProvider; - final FacultyLocationsProvider facultyLocationsProvider; - final HomePageProvider homePageProvider; - final ReferenceProvider referenceProvider; - StateProviders( - this.lectureProvider, - this.examProvider, - this.busStopProvider, - this.restaurantProvider, - this.profileProvider, - this.courseUnitsInfoProvider, - this.sessionProvider, - this.calendarProvider, - this.libraryOccupationProvider, - this.facultyLocationsProvider, - this.homePageProvider, - this.referenceProvider); + this.lectureProvider, + this.examProvider, + this.busStopProvider, + this.restaurantProvider, + this.profileProvider, + this.courseUnitsInfoProvider, + this.sessionProvider, + this.calendarProvider, + this.libraryOccupationProvider, + this.facultyLocationsProvider, + this.homePageProvider, + this.referenceProvider, + ); - static StateProviders fromContext(BuildContext context) { + factory StateProviders.fromContext(BuildContext context) { final lectureProvider = Provider.of(context, listen: false); final examProvider = Provider.of(context, listen: false); @@ -67,17 +55,46 @@ class StateProviders { Provider.of(context, listen: false); return StateProviders( - lectureProvider, - examProvider, - busStopProvider, - restaurantProvider, - profileProvider, - courseUnitsInfoProvider, - sessionProvider, - calendarProvider, - libraryOccupationProvider, - facultyLocationsProvider, - homePageProvider, - referenceProvider); + lectureProvider, + examProvider, + busStopProvider, + restaurantProvider, + profileProvider, + courseUnitsInfoProvider, + sessionProvider, + calendarProvider, + libraryOccupationProvider, + facultyLocationsProvider, + homePageProvider, + referenceProvider, + ); + } + + final LectureProvider lectureProvider; + final ExamProvider examProvider; + final BusStopProvider busStopProvider; + final RestaurantProvider restaurantProvider; + final CourseUnitsInfoProvider courseUnitsInfoProvider; + final ProfileProvider profileProvider; + final SessionProvider sessionProvider; + 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(); } } diff --git a/uni/lib/model/utils/day_of_week.dart b/uni/lib/model/utils/day_of_week.dart index 626620c32..77f78d9d3 100644 --- a/uni/lib/model/utils/day_of_week.dart +++ b/uni/lib/model/utils/day_of_week.dart @@ -9,20 +9,20 @@ enum DayOfWeek { } DayOfWeek? parseDayOfWeek(String str) { - str = str.replaceAll(' ', '').toLowerCase(); - if (str == 'segunda-feira') { + final weekDay = str.replaceAll(' ', '').toLowerCase(); + if (weekDay == 'segunda-feira') { return DayOfWeek.monday; - } else if (str == 'terça-feira') { + } else if (weekDay == 'terça-feira') { return DayOfWeek.tuesday; - } else if (str == 'quarta-feira') { + } else if (weekDay == 'quarta-feira') { return DayOfWeek.wednesday; - } else if (str == 'quinta-feira') { + } else if (weekDay == 'quinta-feira') { return DayOfWeek.thursday; - } else if (str == 'sexta-feira') { + } else if (weekDay == 'sexta-feira') { return DayOfWeek.friday; - } else if (str == 'sábado' || str == 'sabado') { + } else if (weekDay == 'sábado' || weekDay == 'sabado') { return DayOfWeek.saturday; - } else if (str == 'domingo') { + } else if (weekDay == 'domingo') { return DayOfWeek.sunday; } return null; diff --git a/uni/lib/utils/constants.dart b/uni/lib/utils/constants.dart index 78e909eab..868683b34 100644 --- a/uni/lib/utils/constants.dart +++ b/uni/lib/utils/constants.dart @@ -1,5 +1,3 @@ -library constants; - const faculties = [ 'faup', 'fbaup', diff --git a/uni/lib/utils/drawer_items.dart b/uni/lib/utils/drawer_items.dart index a9580353c..bf27ca63d 100644 --- a/uni/lib/utils/drawer_items.dart +++ b/uni/lib/utils/drawer_items.dart @@ -13,11 +13,10 @@ enum DrawerItem { navBugReport('Bugs e Sugestões'), navLogOut('Terminar sessão'); + const DrawerItem(this.title, {this.faculties}); final String title; final Set? faculties; - const DrawerItem(this.title, {this.faculties}); - bool isVisible(List userFaculties) { if (this == DrawerItem.navLogOut) { return false; diff --git a/uni/lib/utils/duration_string_formatter.dart b/uni/lib/utils/duration_string_formatter.dart index 673084283..619684001 100644 --- a/uni/lib/utils/duration_string_formatter.dart +++ b/uni/lib/utils/duration_string_formatter.dart @@ -1,49 +1,62 @@ extension DurationStringFormatter on Duration { static final formattingRegExp = RegExp('{}'); - String toFormattedString(String singularPhrase, String pluralPhrase, - {String term = "{}"}) { + String toFormattedString( + String singularPhrase, + String pluralPhrase, { + String term = '{}', + }) { if (!singularPhrase.contains(term) || !pluralPhrase.contains(term)) { throw ArgumentError( - "singularPhrase or plurarPhrase don't have a string that can be formatted..."); + "singularPhrase or plurarPhrase don't have a string " + 'that can be formatted...', + ); } if (inSeconds == 1) { - return singularPhrase.replaceAll(formattingRegExp, "$inSeconds segundo"); + return singularPhrase.replaceAll(formattingRegExp, '$inSeconds segundo'); } if (inSeconds < 60) { - return pluralPhrase.replaceAll(formattingRegExp, "$inSeconds segundos"); + return pluralPhrase.replaceAll(formattingRegExp, '$inSeconds segundos'); } if (inMinutes == 1) { - return singularPhrase.replaceAll(formattingRegExp, "$inMinutes minuto"); + return singularPhrase.replaceAll(formattingRegExp, '$inMinutes minuto'); } if (inMinutes < 60) { - return pluralPhrase.replaceAll(formattingRegExp, "$inMinutes minutos"); + return pluralPhrase.replaceAll(formattingRegExp, '$inMinutes minutos'); } if (inHours == 1) { - return singularPhrase.replaceAll(formattingRegExp, "$inHours hora"); + return singularPhrase.replaceAll(formattingRegExp, '$inHours hora'); } if (inHours < 24) { - return pluralPhrase.replaceAll(formattingRegExp, "$inHours horas"); + return pluralPhrase.replaceAll(formattingRegExp, '$inHours horas'); } if (inDays == 1) { - return singularPhrase.replaceAll(formattingRegExp, "$inDays dia"); + return singularPhrase.replaceAll(formattingRegExp, '$inDays dia'); } if (inDays <= 7) { - return pluralPhrase.replaceAll(formattingRegExp, "$inDays dias"); + return pluralPhrase.replaceAll(formattingRegExp, '$inDays dias'); } if ((inDays / 7).floor() == 1) { return singularPhrase.replaceAll( - formattingRegExp, "${(inDays / 7).floor()} semana"); + formattingRegExp, + '${(inDays / 7).floor()} semana', + ); } if ((inDays / 7).floor() > 1) { return pluralPhrase.replaceAll( - formattingRegExp, "${(inDays / 7).floor()} semanas"); + formattingRegExp, + '${(inDays / 7).floor()} semanas', + ); } if ((inDays / 30).floor() == 1) { return singularPhrase.replaceAll( - formattingRegExp, "${(inDays / 30).floor()} mês"); + formattingRegExp, + '${(inDays / 30).floor()} mês', + ); } return pluralPhrase.replaceAll( - formattingRegExp, "${(inDays / 30).floor()} meses"); + formattingRegExp, + '${(inDays / 30).floor()} meses', + ); } } diff --git a/uni/lib/utils/favorite_widget_type.dart b/uni/lib/utils/favorite_widget_type.dart index 7af11f41f..aca9f639e 100644 --- a/uni/lib/utils/favorite_widget_type.dart +++ b/uni/lib/utils/favorite_widget_type.dart @@ -3,13 +3,13 @@ enum FavoriteWidgetType { schedule, printBalance, account, - libraryOccupation(faculties: {"feup"}), + libraryOccupation(faculties: {'feup'}), busStops; - final Set? faculties; - const FavoriteWidgetType({this.faculties}); + final Set? faculties; + bool isVisible(List userFaculties) { if (faculties == null) { return true; diff --git a/uni/lib/utils/url_parser.dart b/uni/lib/utils/url_parser.dart index d2d706724..79d13c6ec 100644 --- a/uni/lib/utils/url_parser.dart +++ b/uni/lib/utils/url_parser.dart @@ -1,24 +1,26 @@ Map getUrlQueryParameters(String url) { - final Map queryParameters = {}; + final queryParameters = {}; + var queryString = url; - final int lastSlashIndex = url.lastIndexOf('/'); + final lastSlashIndex = url.lastIndexOf('/'); if (lastSlashIndex >= 0) { - url = url.substring(lastSlashIndex + 1); + queryString = url.substring(lastSlashIndex + 1); } - final int queryStartIndex = url.lastIndexOf('?'); + final queryStartIndex = queryString.lastIndexOf('?'); if (queryStartIndex < 0) { return {}; } - url = url.substring(queryStartIndex + 1); + queryString = queryString.substring(queryStartIndex + 1); - final List params = url.split('&'); - for (String param in params) { - final List keyValue = param.split('='); + 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 1c112c7c9..6badd3e3c 100644 --- a/uni/lib/view/about/about.dart +++ b/uni/lib/view/about/about.dart @@ -14,21 +14,23 @@ class AboutPageView extends StatefulWidget { class AboutPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { - final MediaQueryData queryData = MediaQuery.of(context); + final queryData = MediaQuery.of(context); return ListView( children: [ Padding( - padding: EdgeInsets.only(top: queryData.size.width / 12), - child: SvgPicture.asset( - 'assets/images/logo_ni.svg', - width: queryData.size.height / 7, - height: queryData.size.height / 7, - )), + padding: EdgeInsets.only(top: queryData.size.width / 12), + child: SvgPicture.asset( + 'assets/images/logo_ni.svg', + width: queryData.size.height / 7, + height: queryData.size.height / 7, + ), + ), Center( - child: Padding( - padding: EdgeInsets.all(queryData.size.width / 12), - child: const TermsAndConditions(), - )) + child: Padding( + padding: EdgeInsets.all(queryData.size.width / 12), + child: const TermsAndConditions(), + ), + ) ], ); } diff --git a/uni/lib/view/about/widgets/terms_and_conditions.dart b/uni/lib/view/about/widgets/terms_and_conditions.dart index 4be3ff71f..fb2a62f3b 100644 --- a/uni/lib/view/about/widgets/terms_and_conditions.dart +++ b/uni/lib/view/about/widgets/terms_and_conditions.dart @@ -5,31 +5,31 @@ import 'package:uni/controller/load_static/terms_and_conditions.dart'; import 'package:url_launcher/url_launcher.dart'; class TermsAndConditions extends StatelessWidget { + const TermsAndConditions({super.key}); static String termsAndConditionsSaved = 'Carregando os Termos e Condições...'; - const TermsAndConditions({Key? key}) : super(key: key); - @override Widget build(BuildContext context) { - final Future termsAndConditionsFuture = fetchTermsAndConditions(); + final termsAndConditionsFuture = fetchTermsAndConditions(); return FutureBuilder( - future: termsAndConditionsFuture, - builder: - (BuildContext context, AsyncSnapshot termsAndConditions) { - if (termsAndConditions.connectionState == ConnectionState.done && - termsAndConditions.hasData) { - termsAndConditionsSaved = termsAndConditions.data!; - } - return MarkdownBody( - styleSheet: MarkdownStyleSheet(), - shrinkWrap: false, - data: termsAndConditionsSaved, - onTapLink: (text, url, title) async { - if (await canLaunchUrl(Uri.parse(url!))) { - await launchUrl(Uri.parse(url)); - } - }, - ); - }); + future: termsAndConditionsFuture, + builder: + (BuildContext context, AsyncSnapshot termsAndConditions) { + if (termsAndConditions.connectionState == ConnectionState.done && + termsAndConditions.hasData) { + termsAndConditionsSaved = termsAndConditions.data!; + } + return MarkdownBody( + styleSheet: MarkdownStyleSheet(), + shrinkWrap: false, + data: termsAndConditionsSaved, + onTapLink: (text, url, title) async { + if (await canLaunchUrl(Uri.parse(url!))) { + await launchUrl(Uri.parse(url)); + } + }, + ); + }, + ); } } diff --git a/uni/lib/view/bug_report/bug_report.dart b/uni/lib/view/bug_report/bug_report.dart index eb45bdfa7..f9f7a5c82 100644 --- a/uni/lib/view/bug_report/bug_report.dart +++ b/uni/lib/view/bug_report/bug_report.dart @@ -4,7 +4,7 @@ import 'package:uni/view/bug_report/widgets/form.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; class BugReportPageView extends StatefulWidget { - const BugReportPageView({Key? key}) : super(key: key); + const BugReportPageView({super.key}); @override State createState() => BugReportPageViewState(); @@ -15,8 +15,9 @@ class BugReportPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0), - child: const BugReportForm()); + margin: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), + child: const BugReportForm(), + ); } @override diff --git a/uni/lib/view/bug_report/widgets/form.dart b/uni/lib/view/bug_report/widgets/form.dart index 250a1d75b..ba6748cb6 100644 --- a/uni/lib/view/bug_report/widgets/form.dart +++ b/uni/lib/view/bug_report/widgets/form.dart @@ -24,6 +24,10 @@ class BugReportForm extends StatefulWidget { /// Manages the 'Bugs and Suggestions' section of the app class BugReportFormState extends State { + BugReportFormState() { + loadBugClassList(); + } + final String _gitHubPostUrl = 'https://api.github.com/repos/NIAEFEUP/project-schrodinger/issues'; final String _sentryLink = @@ -36,7 +40,9 @@ class BugReportFormState extends State { 1: const Tuple2('Erro', 'Error'), 2: const Tuple2('Sugestão de funcionalidade', 'Suggestion'), 3: const Tuple2( - 'Comportamento inesperado', 'Unexpected behaviour'), + 'Comportamento inesperado', + 'Unexpected behaviour', + ), 4: const Tuple2('Outro', 'Other'), }; List> bugList = []; @@ -50,84 +56,76 @@ class BugReportFormState extends State { bool _isButtonTapped = false; bool _isConsentGiven = false; - BugReportFormState() { - loadBugClassList(); - } - void loadBugClassList() { bugList = []; - bugDescriptions.forEach((int key, Tuple2 tup) => - {bugList.add(DropdownMenuItem(value: key, child: Text(tup.item1)))}); + bugDescriptions.forEach( + (int key, Tuple2 tup) => + {bugList.add(DropdownMenuItem(value: key, child: Text(tup.item1)))}, + ); } @override Widget build(BuildContext context) { return Form( - key: _formKey, child: ListView(children: getFormWidget(context))); + key: _formKey, + child: ListView(children: getFormWidget(context)), + ); } List getFormWidget(BuildContext context) { - final List formWidget = []; - - formWidget.add(bugReportTitle(context)); - formWidget.add(bugReportIntro(context)); - formWidget.add(dropdownBugSelectWidget(context)); - formWidget.add(FormTextField( - titleController, - Icons.title, - minLines: 1, - maxLines: 2, - description: 'Título', - labelText: 'Breve identificação do problema', - bottomMargin: 30.0, - )); - - formWidget.add(FormTextField( - descriptionController, - Icons.description, - minLines: 1, - maxLines: 30, - description: 'Descrição', - labelText: 'Bug encontrado, como o reproduzir, etc', - bottomMargin: 30.0, - )); - - formWidget.add(FormTextField( - emailController, - Icons.mail, - minLines: 1, - maxLines: 2, - description: 'Contacto (opcional)', - labelText: 'Email em que desejas ser contactado', - bottomMargin: 30.0, - isOptional: true, - formatValidator: (value) { - return EmailValidator.validate(value) - ? null - : 'Por favor insere um email válido'; - }, - )); - - formWidget.add(consentBox(context)); - - formWidget.add(submitButton(context)); - - return formWidget; + return [ + bugReportTitle(context), + bugReportIntro(context), + dropdownBugSelectWidget(context), + FormTextField( + titleController, + Icons.title, + maxLines: 2, + description: 'Título', + labelText: 'Breve identificação do problema', + bottomMargin: 30, + ), + FormTextField( + descriptionController, + Icons.description, + maxLines: 30, + description: 'Descrição', + labelText: 'Bug encontrado, como o reproduzir, etc', + bottomMargin: 30, + ), + FormTextField( + emailController, + Icons.mail, + maxLines: 2, + description: 'Contacto (opcional)', + labelText: 'Email em que desejas ser contactado', + bottomMargin: 30, + isOptional: true, + formatValidator: (String? value) { + return EmailValidator.validate(value ?? '') + ? null + : 'Por favor insere um email válido'; + }, + ), + 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.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: const [ - Icon(Icons.bug_report, size: 40.0), - PageTitle(name: 'Bugs e Sugestões', center: false), - Icon(Icons.bug_report, size: 40.0), - ], - )); + margin: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: const [ + Icon(Icons.bug_report, size: 40), + PageTitle(name: 'Bugs e Sugestões', center: false), + Icon(Icons.bug_report, size: 40), + ], + ), + ); } /// Returns a widget for the overview text of the bug report form @@ -137,10 +135,11 @@ class BugReportFormState extends State { padding: const EdgeInsets.only(bottom: 20), child: Center( child: Text( - '''Encontraste algum bug na aplicação?\nTens alguma ''' - '''sugestão para a app?\nConta-nos para que possamos melhorar!''', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center), + '''Encontraste algum bug na aplicação?\nTens alguma ''' + '''sugestão para a app?\nConta-nos para que possamos melhorar!''', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), ), ); } @@ -158,25 +157,29 @@ class BugReportFormState extends State { style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left, ), - Row(children: [ - Container( + Row( + children: [ + Container( margin: const EdgeInsets.only(right: 15), child: const Icon( Icons.bug_report, - )), - Expanded( + ), + ), + Expanded( child: DropdownButton( - hint: const Text('Tipo de ocorrência'), - items: bugList, - value: _selectedBug, - onChanged: (value) { - setState(() { - _selectedBug = value as int; - }); - }, - isExpanded: true, - )) - ]) + hint: const Text('Tipo de ocorrência'), + items: bugList, + value: _selectedBug, + onChanged: (value) { + setState(() { + _selectedBug = value! as int; + }); + }, + isExpanded: true, + ), + ) + ], + ) ], ), ); @@ -184,15 +187,16 @@ class BugReportFormState extends State { Widget consentBox(BuildContext context) { return Container( - padding: const EdgeInsets.only(), - margin: const EdgeInsets.only(bottom: 20, top: 0), + padding: EdgeInsets.zero, + margin: const EdgeInsets.only(bottom: 20), child: ListTileTheme( - contentPadding: const EdgeInsets.all(0), + contentPadding: EdgeInsets.zero, child: CheckboxListTile( title: Text( - '''Consinto que esta informação seja revista pelo NIAEFEUP, podendo ser eliminada a meu pedido.''', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.left), + '''Consinto que esta informação seja revista pelo NIAEFEUP, podendo ser eliminada a meu pedido.''', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.left, + ), value: _isConsentGiven, onChanged: (bool? newValue) { setState(() { @@ -219,7 +223,7 @@ class BugReportFormState extends State { }, child: const Text( 'Enviar', - style: TextStyle(/*color: Colors.white*/ fontSize: 20.0), + style: TextStyle(/*color: Colors.white*/ fontSize: 20), ), ); } @@ -229,19 +233,18 @@ class BugReportFormState extends State { /// If successful, an issue based on the bug /// report is created in the project repository. /// If unsuccessful, the user receives an error message. - void submitBugReport() async { + Future submitBugReport() async { setState(() { _isButtonTapped = true; }); - final List faculties = - await AppSharedPreferences.getUserFaculties(); + final faculties = await AppSharedPreferences.getUserFaculties(); final bugReport = BugReport( - titleController.text, - descriptionController.text, - emailController.text, - bugDescriptions[_selectedBug], - faculties) - .toMap(); + titleController.text, + descriptionController.text, + emailController.text, + bugDescriptions[_selectedBug], + faculties, + ).toMap(); String toastMsg; bool status; try { @@ -264,8 +267,8 @@ class BugReportFormState extends State { if (mounted) { FocusScope.of(context).requestFocus(FocusNode()); status - ? ToastMessage.success(context, toastMsg) - : ToastMessage.error(context, toastMsg); + ? await ToastMessage.success(context, toastMsg) + : await ToastMessage.error(context, toastMsg); setState(() { _isButtonTapped = false; }); @@ -273,35 +276,41 @@ class BugReportFormState extends State { } Future submitGitHubIssue( - SentryId sentryEvent, Map bugReport) async { - final String description = - '${bugReport['bugLabel']}\nFurther information on: $_sentryLink$sentryEvent'; - final Map data = { + SentryId sentryEvent, + Map bugReport, + ) async { + final description = '${bugReport['bugLabel']}\nFurther information on: ' + '$_sentryLink$sentryEvent'; + final data = { 'title': bugReport['title'], 'body': description, 'labels': ['In-app bug report', bugReport['bugLabel']], }; - for (String faculty in bugReport['faculties']) { - data['labels'].add(faculty); + for (final faculty in bugReport['faculties'] as Iterable) { + (data['labels'] as List).add(faculty); } return http - .post(Uri.parse(_gitHubPostUrl), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'token ${dotenv.env["GH_TOKEN"]}}' - }, - body: json.encode(data)) + .post( + Uri.parse(_gitHubPostUrl), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'token ${dotenv.env["GH_TOKEN"]}}' + }, + body: json.encode(data), + ) .then((http.Response response) { return response.statusCode; }); } Future submitSentryEvent(Map bugReport) async { - final String description = bugReport['email'] == '' + final description = bugReport['email'] == '' ? '${bugReport['text']} from ${bugReport['faculty']}' - : '${bugReport['text']} from ${bugReport['faculty']}\nContact: ${bugReport['email']}'; + : '${bugReport['text']} from ${bugReport['faculty']}\nContact: ' + '${bugReport['email']}'; return Sentry.captureMessage( - '${bugReport['bugLabel']}: ${bugReport['text']}\n$description'); + '${bugReport['bugLabel']}: ${bugReport['text']}\n$description', + ); } void clearForm() { diff --git a/uni/lib/view/bug_report/widgets/text_field.dart b/uni/lib/view/bug_report/widgets/text_field.dart index ae021e20c..1ff858a90 100644 --- a/uni/lib/view/bug_report/widgets/text_field.dart +++ b/uni/lib/view/bug_report/widgets/text_field.dart @@ -1,6 +1,20 @@ import 'package:flutter/material.dart'; class FormTextField extends StatelessWidget { + const FormTextField( + this.controller, + this.icon, { + this.description = '', + this.minLines = 1, + this.maxLines = 1, + this.labelText = '', + this.hintText = '', + this.emptyText = 'Por favor preenche este campo', + this.bottomMargin = 0, + this.isOptional = false, + this.formatValidator, + super.key, + }); final TextEditingController controller; final IconData icon; final String description; @@ -11,20 +25,7 @@ class FormTextField extends StatelessWidget { final int maxLines; final double bottomMargin; final bool isOptional; - final Function? formatValidator; - - const FormTextField(this.controller, this.icon, - {this.description = '', - this.minLines = 1, - this.maxLines = 1, - this.labelText = '', - this.hintText = '', - this.emptyText = 'Por favor preenche este campo', - this.bottomMargin = 0, - this.isOptional = false, - this.formatValidator, - Key? key}) - : super(key: key); + final String? Function(String?)? formatValidator; @override Widget build(BuildContext context) { @@ -38,33 +39,37 @@ class FormTextField extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left, ), - Row(children: [ - Container( + Row( + children: [ + Container( margin: const EdgeInsets.only(right: 15), child: Icon( icon, - )), - Expanded( - child: TextFormField( - // margins - minLines: minLines, - maxLines: maxLines, - decoration: InputDecoration( - focusedBorder: const UnderlineInputBorder(), - hintText: hintText, - hintStyle: Theme.of(context).textTheme.bodyMedium, - labelText: labelText, - labelStyle: Theme.of(context).textTheme.bodyMedium, + ), ), - controller: controller, - validator: (value) { - if (value!.isEmpty) { - return isOptional ? null : emptyText; - } - return formatValidator != null ? formatValidator!(value) : null; - }, - )) - ]) + Expanded( + child: TextFormField( + // margins + minLines: minLines, + maxLines: maxLines, + decoration: InputDecoration( + focusedBorder: const UnderlineInputBorder(), + hintText: hintText, + hintStyle: Theme.of(context).textTheme.bodyMedium, + labelText: labelText, + labelStyle: Theme.of(context).textTheme.bodyMedium, + ), + controller: controller, + validator: (String? value) { + if (value == null || value.isEmpty) { + return isOptional ? null : emptyText; + } + return formatValidator?.call(value); + }, + ), + ) + ], + ) ], ), ); diff --git a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart index 56688a6f8..880c17865 100644 --- a/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart +++ b/uni/lib/view/bus_stop_next_arrivals/bus_stop_next_arrivals.dart @@ -3,16 +3,16 @@ import 'package:provider/provider.dart'; import 'package:uni/model/entities/bus_stop.dart'; import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/model/request_status.dart'; -import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/bus_stop_row.dart'; import 'package:uni/view/bus_stop_selection/bus_stop_selection.dart'; +import 'package:uni/view/common_widgets/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/lazy_consumer.dart'; class BusStopNextArrivalsPage extends StatefulWidget { - const BusStopNextArrivalsPage({Key? key}) : super(key: key); + const BusStopNextArrivalsPage({super.key}); @override State createState() => BusStopNextArrivalsPageState(); @@ -24,9 +24,12 @@ class BusStopNextArrivalsPageState @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, busProvider) => ListView(children: [ - NextArrivals(busProvider.configuredBusStops, busProvider.status) - ])); + builder: (context, busProvider) => ListView( + children: [ + NextArrivals(busProvider.configuredBusStops, busProvider.status) + ], + ), + ); } @override @@ -37,12 +40,11 @@ class BusStopNextArrivalsPageState } class NextArrivals extends StatefulWidget { + const NextArrivals(this.buses, this.busStopStatus, {super.key}); //final Map> trips; final Map buses; final RequestStatus busStopStatus; - const NextArrivals(this.buses, this.busStopStatus, {super.key}); - @override NextArrivalsState createState() => NextArrivalsState(); } @@ -55,124 +57,143 @@ class NextArrivalsState extends State { switch (widget.busStopStatus) { case RequestStatus.successful: return SizedBox( - height: MediaQuery.of(context).size.height, - child: Column(children: requestSuccessful(context))); + 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))); + height: MediaQuery.of(context).size.height, + child: Column(children: requestBusy(context)), + ); case RequestStatus.failed: return SizedBox( - height: MediaQuery.of(context).size.height, - child: Column(children: requestFailed(context))); - default: + height: MediaQuery.of(context).size.height, + child: Column(children: requestFailed(context)), + ); + case RequestStatus.none: return Container(); } } return DefaultTabController( - length: widget.buses.length, child: contentBuilder()); + length: widget.buses.length, + child: contentBuilder(), + ); } /// Returns a list of widgets for a successfull request - List requestSuccessful(context) { - final List result = []; - - result.addAll(getHeader(context)); + 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: 'Não percas nenhum autocarro', - labelTextStyle: TextStyle( + result + ..add( + ImageLabel( + imagePath: 'assets/images/bus.png', + label: 'Não percas nenhum autocarro', + labelTextStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 17, - color: Theme.of(context).colorScheme.primary))); - result.add(Column(children: [ - ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage())), - child: const Text('Adicionar'), - ), - ])); + color: Theme.of(context).colorScheme.primary, + ), + ), + ) + ..add( + Column( + children: [ + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BusStopSelectionPage(), + ), + ), + child: const Text('Adicionar'), + ), + ], + ), + ); } return result; } - /// TODO: Is this ok? /// Returns a list of widgets for a busy request + // TODO(bdmendes): Is this ok? List requestBusy(BuildContext context) { - final List result = []; - - result.add(getPageTitle()); - result.add(Container( - padding: const EdgeInsets.all(22.0), - child: const Center(child: CircularProgressIndicator()))); - - return result; + return [ + getPageTitle(), + Container( + padding: const EdgeInsets.all(22), + child: const Center(child: CircularProgressIndicator()), + ), + ]; } Container getPageTitle() { return Container( - padding: const EdgeInsets.only(bottom: 12.0), - child: const PageTitle(name: 'Autocarros')); + padding: const EdgeInsets.only(bottom: 12), + child: const PageTitle(name: 'Autocarros'), + ); } /// Returns a list of widgets for a failed request List requestFailed(BuildContext context) { - final List result = []; - - result.addAll(getHeader(context)); - result.add(Container( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text('Não foi possível obter informação', - maxLines: 2, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium))); - - return result; + 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(context) { + List getHeader(BuildContext context) { return [ getPageTitle(), Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.only(left: 10.0), - child: const LastUpdateTimeStamp(), + 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(), + ), ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BusStopSelectionPage()))) - ]), + ) + ], + ), ) ]; } List getContent(BuildContext context) { - final MediaQueryData queryData = MediaQuery.of(context); + final queryData = MediaQuery.of(context); return [ Container( decoration: const BoxDecoration( border: Border( - bottom: BorderSide(width: 1.0), + bottom: BorderSide(), ), ), - constraints: const BoxConstraints(maxHeight: 150.0), + constraints: const BoxConstraints(maxHeight: 150), child: Material( child: TabBar( isScrollable: true, @@ -182,7 +203,7 @@ class NextArrivalsState extends State { ), Expanded( child: Container( - padding: const EdgeInsets.only(bottom: 92.0), + padding: const EdgeInsets.only(bottom: 92), child: TabBarView( children: getEachBusStopInfo(context), ), @@ -191,33 +212,44 @@ class NextArrivalsState extends State { ]; } - List createTabs(queryData) { - final List tabs = []; + List createTabs(MediaQueryData queryData) { + final tabs = []; widget.buses.forEach((stopCode, stopData) { - tabs.add(SizedBox( - width: queryData.size.width / - ((widget.buses.length < 3 ? widget.buses.length : 3) + 1), - child: Tab(text: stopCode), - )); + tabs.add( + SizedBox( + width: queryData.size.width / + ((widget.buses.length < 3 ? widget.buses.length : 3) + 1), + child: Tab(text: stopCode), + ), + ); }); return tabs; } /// Returns a list of widgets, for each bus stop configured by the user - List getEachBusStopInfo(context) { - final List rows = []; + List getEachBusStopInfo(BuildContext context) { + final rows = []; widget.buses.forEach((stopCode, stopData) { - rows.add(ListView(children: [ - Container( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 22.0, right: 22.0), - child: BusStopRow( - stopCode: stopCode, - trips: widget.buses[stopCode]?.trips ?? [], - stopCodeShow: false, - )) - ])); + rows.add( + ListView( + children: [ + Container( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 22, + right: 22, + ), + child: BusStopRow( + stopCode: stopCode, + trips: widget.buses[stopCode]?.trips ?? [], + stopCodeShow: false, + ), + ) + ], + ), + ); }); return rows; 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 ce09b350b..1cdbb688f 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 @@ -3,23 +3,22 @@ import 'package:uni/model/entities/trip.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/trip_row.dart'; class BusStopRow extends StatelessWidget { - final String stopCode; - final List trips; - final bool stopCodeShow; - final bool singleTrip; - const BusStopRow({ - Key? key, required this.stopCode, required this.trips, + super.key, this.singleTrip = false, this.stopCodeShow = true, - }) : super(key: key); + }); + final String stopCode; + final List trips; + final bool stopCodeShow; + final bool singleTrip; @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: getTrips(context), @@ -27,8 +26,8 @@ class BusStopRow extends StatelessWidget { ); } - List getTrips(context) { - final List row = []; + List getTrips(BuildContext context) { + final row = []; if (stopCodeShow) { row.add(stopCodeRotatedContainer(context)); @@ -37,7 +36,7 @@ class BusStopRow extends StatelessWidget { if (trips.isEmpty) { row.add(noTripsContainer(context)); } else { - final List tripRows = getTripRows(context); + final tripRows = getTripRows(context); row.add(Expanded(child: Column(children: tripRows))); } @@ -45,19 +44,22 @@ class BusStopRow extends StatelessWidget { return row; } - Widget noTripsContainer(context) { + Widget noTripsContainer(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Text('Não há viagens planeadas de momento.', - maxLines: 3, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium)); + padding: const EdgeInsets.symmetric(vertical: 20), + child: Text( + 'Não há viagens planeadas de momento.', + maxLines: 3, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ); } - Widget stopCodeRotatedContainer(context) { + Widget stopCodeRotatedContainer(BuildContext context) { return Container( - padding: const EdgeInsets.only(left: 4.0), + padding: const EdgeInsets.only(left: 4), child: RotatedBox( quarterTurns: 3, child: Text(stopCode, style: Theme.of(context).textTheme.titleMedium), @@ -66,24 +68,33 @@ class BusStopRow extends StatelessWidget { } List getTripRows(BuildContext context) { - final List tripRows = []; + final tripRows = []; if (singleTrip) { - tripRows.add(Container( - padding: const EdgeInsets.all(12.0), child: TripRow(trip: trips[0]))); + tripRows.add( + Container( + padding: const EdgeInsets.all(12), + child: TripRow(trip: trips[0]), + ), + ); } else { - for (int i = 0; i < trips.length; i++) { + 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.0), + tripRows.add( + Container( + padding: const EdgeInsets.all(12), decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.1, /* color: color */ - ))), - child: TripRow(trip: trips[i]))); + border: Border( + bottom: BorderSide( + width: 0.1, /* color: color */ + ), + ), + ), + child: TripRow(trip: trips[i]), + ), + ); } } 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 d42df38a1..37c6ca6c8 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 @@ -4,12 +4,11 @@ import 'package:uni/view/lazy_consumer.dart'; /// Manages the section with the estimated time for the bus arrival class EstimatedArrivalTimeStamp extends StatelessWidget { - final String timeRemaining; - const EstimatedArrivalTimeStamp({ - Key? key, required this.timeRemaining, - }) : super(key: key); + super.key, + }); + final String timeRemaining; @override Widget build(BuildContext context) { @@ -20,15 +19,17 @@ class EstimatedArrivalTimeStamp extends StatelessWidget { } Widget getContent(BuildContext context, DateTime timeStamp) { - final DateTime estimatedTime = + final estimatedTime = timeStamp.add(Duration(minutes: int.parse(timeRemaining), seconds: 30)); - int num = estimatedTime.hour; - final String hour = (num >= 10 ? '$num' : '0$num'); + var num = estimatedTime.hour; + final hour = num >= 10 ? '$num' : '0$num'; num = estimatedTime.minute; - final String minute = (num >= 10 ? '$num' : '0$num'); + final minute = num >= 10 ? '$num' : '0$num'; - return Text('$hour:$minute', - style: Theme.of(context).textTheme.titleMedium); + return Text( + '$hour:$minute', + style: Theme.of(context).textTheme.titleMedium, + ); } } diff --git a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart index 391146f72..3a05de40e 100644 --- a/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart +++ b/uni/lib/view/bus_stop_next_arrivals/widgets/trip_row.dart @@ -3,12 +3,11 @@ import 'package:uni/model/entities/trip.dart'; import 'package:uni/view/bus_stop_next_arrivals/widgets/estimated_arrival_timestamp.dart'; class TripRow extends StatelessWidget { - final Trip trip; - const TripRow({ - Key? key, required this.trip, - }) : super(key: key); + super.key, + }); + final Trip trip; @override Widget build(BuildContext context) { @@ -18,20 +17,30 @@ class TripRow extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(trip.line, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium), - Text(trip.destination, - style: Theme.of(context).textTheme.titleMedium), + Text( + trip.line, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + trip.destination, + style: Theme.of(context).textTheme.titleMedium, + ), ], ), - Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('${trip.timeRemaining}\'', - style: Theme.of(context).textTheme.titleMedium), - EstimatedArrivalTimeStamp( - timeRemaining: trip.timeRemaining.toString()), - ]) + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${trip.timeRemaining}'", + style: Theme.of(context).textTheme.titleMedium, + ), + EstimatedArrivalTimeStamp( + timeRemaining: trip.timeRemaining.toString(), + ), + ], + ) ], ); } 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 21c7b6dd1..34bb85649 100644 --- a/uni/lib/view/bus_stop_selection/bus_stop_selection.dart +++ b/uni/lib/view/bus_stop_selection/bus_stop_selection.dart @@ -18,7 +18,7 @@ class BusStopSelectionPage extends StatefulWidget { /// Manages the 'Bus stops' section of the app. class BusStopSelectionPageState extends SecondaryPageViewState { - final double borderRadius = 15.0; + final double borderRadius = 15; final DateTime now = DateTime.now(); final db = AppBusStopDatabase(); @@ -26,7 +26,7 @@ class BusStopSelectionPageState final List suggestionsList = []; List getStopsTextList() { - final List stops = []; + final stops = []; configuredStops.forEach((stopCode, stopData) { stops.add(Text(stopCode)); }); @@ -36,41 +36,51 @@ class BusStopSelectionPageState @override Widget getBody(BuildContext context) { final width = MediaQuery.of(context).size.width; - return LazyConsumer(builder: (context, busProvider) { - final List rows = []; - busProvider.configuredBusStops.forEach((stopCode, stopData) => - rows.add(BusStopSelectionRow(stopCode, stopData))); - return ListView( + return LazyConsumer( + builder: (context, busProvider) { + final rows = []; + busProvider.configuredBusStops.forEach( + (stopCode, stopData) => + rows.add(BusStopSelectionRow(stopCode, stopData)), + ); + return ListView( padding: const EdgeInsets.only( bottom: 20, ), children: [ const PageTitle(name: 'Autocarros Configurados'), Container( - padding: const EdgeInsets.all(20.0), - child: const Text( - '''Os autocarros favoritos serão apresentados no widget 'Autocarros' dos favoritos. ''' - '''Os restantes serão apresentados apenas na página.''', - textAlign: TextAlign.center)), + padding: const EdgeInsets.all(20), + child: const Text( + '''Os autocarros favoritos serão apresentados no widget 'Autocarros' dos favoritos. ''' + '''Os restantes serão apresentados apenas na página.''', + textAlign: TextAlign.center, + ), + ), Column(children: rows), Container( - padding: - EdgeInsets.only(left: width * 0.20, right: width * 0.20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => showSearch( - context: context, delegate: BusStopSearch()), - child: const Text('Adicionar'), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text('Concluído'), - ), - ])) - ]); - }); + padding: EdgeInsets.only(left: width * 0.20, right: width * 0.20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () => showSearch( + context: context, + delegate: BusStopSearch(), + ), + child: const Text('Adicionar'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text('Concluído'), + ), + ], + ), + ) + ], + ); + }, + ); } @override 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 a5f946f38..9f24bccb4 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 @@ -12,15 +12,15 @@ import 'package:uni/view/bus_stop_selection/widgets/form.dart'; /// Manages the section of the app displayed when the /// user searches for a bus stop class BusStopSearch extends SearchDelegate { + BusStopSearch() { + getDatabase(); + } + List suggestionsList = []; late final AppBusStopDatabase db; String? stopCode; BusStopData? stopData; - BusStopSearch() { - getDatabase(); - } - Future getDatabase() async { db = AppBusStopDatabase(); } @@ -29,10 +29,11 @@ class BusStopSearch extends SearchDelegate { List buildActions(BuildContext context) { return [ IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - query = ''; - }) + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ) ]; } @@ -41,10 +42,11 @@ class BusStopSearch extends SearchDelegate { //Back arrow to go back to menu return IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.pop(context); - }); + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ); } @override @@ -62,49 +64,63 @@ class BusStopSearch extends SearchDelegate { if (suggestionsList.isEmpty) return ListView(); return ListView.builder( itemBuilder: (context, index) => ListTile( - onTap: () { - Navigator.pop(context); - showDialog( - context: context, - builder: (BuildContext context) { - return busListing(context, suggestionsList[index]); - }); - }, - leading: const Icon(Icons.directions_bus), - title: Text(suggestionsList[index])), + onTap: () { + Navigator.pop(context); + showDialog( + context: context, + builder: (BuildContext context) { + return busListing(context, suggestionsList[index]); + }, + ); + }, + leading: const Icon(Icons.directions_bus), + title: Text(suggestionsList[index]), + ), itemCount: min(suggestionsList.length, 9), ); } Widget busListing(BuildContext context, String suggestion) { - final BusesForm busesForm = BusesForm( - suggestion.splitMapJoin(RegExp(r'\[[A-Z0-9_]+\]'), - onMatch: (m) => m.group(0)!.substring(1, m.group(0)!.length - 1), - onNonMatch: (m) => ''), - updateStopCallback); + final busesForm = BusesForm( + suggestion.splitMapJoin( + RegExp(r'\[[A-Z0-9_]+\]'), + onMatch: (m) => m.group(0)!.substring(1, m.group(0)!.length - 1), + onNonMatch: (m) => '', + ), + updateStopCallback, + ); return AlertDialog( - title: Text('Seleciona os autocarros dos quais queres informação:', - style: Theme.of(context).textTheme.headlineSmall), - content: SizedBox( - height: 200.0, - width: 100.0, - child: busesForm, + title: Text( + 'Seleciona os autocarros dos quais queres informação:', + style: Theme.of(context).textTheme.headlineSmall, + ), + content: SizedBox( + height: 200, + width: 100, + child: busesForm, + ), + actions: [ + TextButton( + child: Text( + 'Cancelar', + style: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () => Navigator.pop(context), ), - actions: [ - TextButton( - child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), - onPressed: () => Navigator.pop(context)), - ElevatedButton( - child: const Text('Confirmar'), - onPressed: () async { - if (stopData!.configuredBuses.isNotEmpty) { - Provider.of(context, listen: false) - .addUserBusStop(stopCode!, stopData!); - Navigator.pop(context); - } - }) - ]); + ElevatedButton( + child: const Text('Confirmar'), + onPressed: () async { + if (stopData!.configuredBuses.isNotEmpty) { + await Provider.of(context, listen: false) + .addUserBusStop(stopCode!, stopData!); + if (context.mounted) { + Navigator.pop(context); + } + } + }, + ) + ], + ); } /// Returns a widget for the suggestions list displayed to the user. @@ -123,11 +139,12 @@ class BusStopSearch extends SearchDelegate { !snapshot.hasError) { if (snapshot.data!.isEmpty) { return Container( - margin: const EdgeInsets.all(8.0), - height: 24.0, - child: const Center( - child: Text('Sem resultados.'), - )); + margin: const EdgeInsets.all(8), + height: 24, + child: const Center( + child: Text('Sem resultados.'), + ), + ); } else { suggestionsList = snapshot.data!; } @@ -136,7 +153,6 @@ class BusStopSearch extends SearchDelegate { } return getSuggestionList(context); }, - initialData: null, ); } diff --git a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart index 62ac1f29a..66621e86c 100644 --- a/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart +++ b/uni/lib/view/bus_stop_selection/widgets/bus_stop_selection_row.dart @@ -7,11 +7,10 @@ import 'package:uni/model/providers/lazy/bus_stop_provider.dart'; import 'package:uni/view/common_widgets/row_container.dart'; class BusStopSelectionRow extends StatefulWidget { + const BusStopSelectionRow(this.stopCode, this.stopData, {super.key}); final String stopCode; final BusStopData stopData; - const BusStopSelectionRow(this.stopCode, this.stopData, {super.key}); - @override State createState() => BusStopSelectionRowState(); } @@ -19,14 +18,18 @@ class BusStopSelectionRow extends StatefulWidget { class BusStopSelectionRowState extends State { BusStopSelectionRowState(); - Future deleteStop(BuildContext context) async { - Provider.of(context, listen: false) - .removeUserBusStop(widget.stopCode); + Future deleteStop(BuildContext context) async { + unawaited( + Provider.of(context, listen: false) + .removeUserBusStop(widget.stopCode), + ); } - Future toggleFavorite(BuildContext context) async { - Provider.of(context, listen: false) - .toggleFavoriteUserBusStop(widget.stopCode, widget.stopData); + Future toggleFavorite(BuildContext context) async { + unawaited( + Provider.of(context, listen: false) + .toggleFavoriteUserBusStop(widget.stopCode, widget.stopData), + ); } @override @@ -34,30 +37,41 @@ class BusStopSelectionRowState extends State { final width = MediaQuery.of(context).size.width; return Container( - padding: EdgeInsets.only( - top: 8.0, bottom: 8.0, left: width * 0.20, right: width * 0.20), - child: RowContainer( - child: Container( - padding: const EdgeInsets.only(left: 10.0, right: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(widget.stopCode), - Row(children: [ - GestureDetector( - child: Icon( - widget.stopData.favorited - ? Icons.star - : Icons.star_border, - ), - onTap: () => toggleFavorite(context)), - IconButton( - icon: const Icon(Icons.cancel), - onPressed: () { - deleteStop(context); - }, - ) - ]) - ])))); + padding: EdgeInsets.only( + top: 8, + bottom: 8, + left: width * 0.20, + right: width * 0.20, + ), + child: RowContainer( + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.stopCode), + Row( + children: [ + GestureDetector( + child: Icon( + widget.stopData.favorited + ? Icons.star + : Icons.star_border, + ), + onTap: () => toggleFavorite(context), + ), + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + deleteStop(context); + }, + ) + ], + ) + ], + ), + ), + ), + ); } } diff --git a/uni/lib/view/bus_stop_selection/widgets/form.dart b/uni/lib/view/bus_stop_selection/widgets/form.dart index 9c00f68b4..b7ecb6ba7 100644 --- a/uni/lib/view/bus_stop_selection/widgets/form.dart +++ b/uni/lib/view/bus_stop_selection/widgets/form.dart @@ -6,11 +6,9 @@ import 'package:uni/model/entities/bus_stop.dart'; 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 Function updateStopCallback; - - const BusesForm(this.stopCode, this.updateStopCallback, {Key? key}) - : super(key: key); + final void Function(String, BusStopData) updateStopCallback; @override State createState() { @@ -28,21 +26,19 @@ class BusesFormState extends State { getStopBuses(); } - void getStopBuses() async { - final List buses = - await DeparturesFetcher.getBusesStoppingAt(widget.stopCode); + Future getStopBuses() async { + final buses = await DeparturesFetcher.getBusesStoppingAt(widget.stopCode); setState(() { this.buses = buses; busesToAdd.fillRange(0, buses.length, false); }); if (!mounted) return; - final BusStopData? currentConfig = - Provider.of(context, listen: false) - .configuredBusStops[widget.stopCode]; + final currentConfig = Provider.of(context, listen: false) + .configuredBusStops[widget.stopCode]; if (currentConfig == null) { return; } - for (int i = 0; i < buses.length; i++) { + for (var i = 0; i < buses.length; i++) { if (currentConfig.configuredBuses.contains(buses[i].busCode)) { busesToAdd[i] = true; } @@ -53,34 +49,40 @@ class BusesFormState extends State { Widget build(BuildContext context) { updateBusStop(); return ListView( - children: List.generate(buses.length, (i) { - return CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - title: Text('[${buses[i].busCode}] ${buses[i].destination}', - overflow: TextOverflow.fade, softWrap: false), + children: List.generate(buses.length, (i) { + return CheckboxListTile( + contentPadding: EdgeInsets.zero, + title: Text( + '[${buses[i].busCode}] ${buses[i].destination}', + overflow: TextOverflow.fade, + softWrap: false, + ), value: busesToAdd[i], onChanged: (value) { setState(() { busesToAdd[i] = value!; }); - }); - })); + }, + ); + }), + ); } void updateBusStop() { - final BusStopData? currentConfig = - Provider.of(context, listen: false) - .configuredBusStops[widget.stopCode]; - final Set newBuses = {}; - for (int i = 0; i < buses.length; i++) { + final currentConfig = Provider.of(context, listen: false) + .configuredBusStops[widget.stopCode]; + final newBuses = {}; + for (var i = 0; i < buses.length; i++) { if (busesToAdd[i]) { newBuses.add(buses[i].busCode); } } widget.updateStopCallback( - widget.stopCode, - BusStopData( - configuredBuses: newBuses, - favorited: currentConfig == null ? true : currentConfig.favorited)); + widget.stopCode, + BusStopData( + configuredBuses: newBuses, + favorited: currentConfig == null || currentConfig.favorited, + ), + ); } } diff --git a/uni/lib/view/calendar/calendar.dart b/uni/lib/view/calendar/calendar.dart index ffd8791b7..48b0ef1fa 100644 --- a/uni/lib/view/calendar/calendar.dart +++ b/uni/lib/view/calendar/calendar.dart @@ -9,7 +9,7 @@ import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/lazy_consumer.dart'; class CalendarPageView extends StatefulWidget { - const CalendarPageView({Key? key}) : super(key: key); + const CalendarPageView({super.key}); @override State createState() => CalendarPageViewState(); @@ -19,23 +19,30 @@ class CalendarPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, calendarProvider) => ListView(children: [ - _getPageTitle(), - RequestDependentWidgetBuilder( - status: calendarProvider.status, - builder: () => - getTimeline(context, calendarProvider.calendar), - hasContentPredicate: calendarProvider.calendar.isNotEmpty, - onNullContent: const Center( - child: Text('Nenhum evento encontrado', - style: TextStyle(fontSize: 18.0)))) - ])); + builder: (context, calendarProvider) => ListView( + children: [ + _getPageTitle(), + RequestDependentWidgetBuilder( + status: calendarProvider.status, + builder: () => getTimeline(context, calendarProvider.calendar), + hasContentPredicate: calendarProvider.calendar.isNotEmpty, + onNullContent: const Center( + child: Text( + 'Nenhum evento encontrado', + style: TextStyle(fontSize: 18), + ), + ), + ) + ], + ), + ); } Widget _getPageTitle() { return Container( - padding: const EdgeInsets.only(bottom: 6.0), - child: const PageTitle(name: 'Calendário Escolar')); + padding: const EdgeInsets.only(bottom: 6), + child: const PageTitle(name: 'Calendário Escolar'), + ); } Widget getTimeline(BuildContext context, List calendar) { @@ -43,27 +50,31 @@ class CalendarPageViewState extends GeneralPageViewState { theme: TimelineTheme.of(context).copyWith( connectorTheme: TimelineTheme.of(context) .connectorTheme - .copyWith(thickness: 2.0, color: Theme.of(context).dividerColor), + .copyWith(thickness: 2, color: Theme.of(context).dividerColor), indicatorTheme: TimelineTheme.of(context) .indicatorTheme - .copyWith(size: 15.0, color: Theme.of(context).primaryColor), + .copyWith(size: 15, color: Theme.of(context).primaryColor), ), builder: TimelineTileBuilder.fromStyle( contentsAlign: ContentsAlign.alternating, contentsBuilder: (context, index) => Padding( - padding: const EdgeInsets.all(24.0), - child: Text(calendar[index].name, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.w500)), + padding: const EdgeInsets.all(24), + child: Text( + calendar[index].name, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.w500), + ), ), oppositeContentsBuilder: (context, index) => Padding( - padding: const EdgeInsets.all(24.0), - child: Text(calendar[index].date, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontStyle: FontStyle.italic, - )), + padding: const EdgeInsets.all(24), + child: Text( + calendar[index].date, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontStyle: FontStyle.italic, + ), + ), ), itemCount: calendar.length, ), diff --git a/uni/lib/view/common_widgets/date_rectangle.dart b/uni/lib/view/common_widgets/date_rectangle.dart index b2e1a7d3d..cbfbe3ed1 100644 --- a/uni/lib/view/common_widgets/date_rectangle.dart +++ b/uni/lib/view/common_widgets/date_rectangle.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; /// Display the current date. /// -/// Example: The rectangular section with the text "last update at [time]". +/// Example: The rectangular section with the text "last update at [date]". class DateRectangle extends StatelessWidget { + const DateRectangle({required this.date, super.key}); final String date; - const DateRectangle({Key? key, required this.date}) : super(key: key); - @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(4), margin: const EdgeInsets.only(bottom: 10), alignment: Alignment.center, width: double.infinity, diff --git a/uni/lib/view/common_widgets/expanded_image_label.dart b/uni/lib/view/common_widgets/expanded_image_label.dart index f4a60e0d0..d2ffc4e07 100644 --- a/uni/lib/view/common_widgets/expanded_image_label.dart +++ b/uni/lib/view/common_widgets/expanded_image_label.dart @@ -1,25 +1,23 @@ import 'package:flutter/material.dart'; class ImageLabel extends StatelessWidget { + const ImageLabel({ + required this.imagePath, + required this.label, + super.key, + this.labelTextStyle, + this.sublabel = '', + this.sublabelTextStyle, + }); final String imagePath; final String label; final TextStyle? labelTextStyle; final String sublabel; final TextStyle? sublabelTextStyle; - const ImageLabel( - {Key? key, - required this.imagePath, - required this.label, - this.labelTextStyle, - this.sublabel = '', - this.sublabelTextStyle}) - : super(key: key); - @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( imagePath, diff --git a/uni/lib/view/common_widgets/generic_card.dart b/uni/lib/view/common_widgets/generic_card.dart index d85ef74a1..dd94e106e 100644 --- a/uni/lib/view/common_widgets/generic_card.dart +++ b/uni/lib/view/common_widgets/generic_card.dart @@ -3,25 +3,30 @@ import 'package:uni/model/entities/time_utilities.dart'; /// App default card abstract class GenericCard extends StatefulWidget { + GenericCard({Key? key}) + : this.customStyle(key: key, editingMode: false, onDelete: () {}); + + const GenericCard.fromEditingInformation( + Key key, { + required bool editingMode, + void Function()? onDelete, + }) : this.customStyle( + key: key, + editingMode: editingMode, + onDelete: onDelete, + ); + + const GenericCard.customStyle({ + required this.editingMode, + required this.onDelete, + super.key, + this.margin = const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + this.hasSmallTitle = false, + }); final EdgeInsetsGeometry margin; final bool hasSmallTitle; final bool editingMode; - final Function()? onDelete; - - GenericCard({Key? key}) - : this.customStyle(key: key, editingMode: false, onDelete: () => null); - - const GenericCard.fromEditingInformation(Key key, editingMode, onDelete) - : this.customStyle( - key: key, editingMode: editingMode, onDelete: onDelete); - - const GenericCard.customStyle( - {Key? key, - required this.editingMode, - required this.onDelete, - this.margin = const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - this.hasSmallTitle = false}) - : super(key: key); + final void Function()? onDelete; @override State createState() { @@ -37,12 +42,14 @@ abstract class GenericCard extends StatefulWidget { void onRefresh(BuildContext context); Text getInfoText(String text, BuildContext context) { - return Text(text, - textAlign: TextAlign.end, - style: Theme.of(context).textTheme.titleLarge!); + return Text( + text, + textAlign: TextAlign.end, + style: Theme.of(context).textTheme.titleLarge, + ); } - showLastRefreshedTime(String? time, context) { + StatelessWidget showLastRefreshedTime(String? time, BuildContext context) { if (time == null) { return const Text('N/A'); } @@ -53,109 +60,125 @@ abstract class GenericCard extends StatefulWidget { } return Container( - alignment: Alignment.center, - child: Text('última atualização às ${parsedTime.toTimeHourMinString()}', - style: Theme.of(context).textTheme.bodySmall)); + alignment: Alignment.center, + child: Text( + 'última atualização às ${parsedTime.toTimeHourMinString()}', + style: Theme.of(context).textTheme.bodySmall, + ), + ); } } class GenericCardState extends State { - final double borderRadius = 10.0; - final double padding = 12.0; + final double borderRadius = 10; + final double padding = 12; @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - if (!widget.editingMode) { - widget.onClick(context); - } - }, - child: Card( - margin: widget.margin, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius)), + onTap: () { + if (!widget.editingMode) { + widget.onClick(context); + } + }, + child: Card( + margin: widget.margin, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color.fromARGB(0x1c, 0, 0, 0), + blurRadius: 7, + offset: Offset(0, 1), + ) + ], + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 60, + ), child: Container( - decoration: BoxDecoration(boxShadow: const [ - BoxShadow( - color: Color.fromARGB(0x1c, 0, 0, 0), - blurRadius: 7.0, - offset: Offset(0.0, 1.0)) - ], borderRadius: BorderRadius.all(Radius.circular(borderRadius))), - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: 60.0, - ), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: - BorderRadius.all(Radius.circular(borderRadius))), - width: (double.infinity), - child: Column( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + ), + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 15), - margin: const EdgeInsets.only(top: 15, bottom: 10), - child: Text(widget.getTitle(), - style: (widget.hasSmallTitle - ? Theme.of(context) - .textTheme - .titleLarge! - : Theme.of(context) - .textTheme - .headlineSmall!) - .copyWith( - color: Theme.of(context).primaryColor)), - )), - if (widget.editingMode) - Container( - alignment: Alignment.center, - margin: const EdgeInsets.only(top: 8), - child: getMoveIcon(context), + children: [ + Flexible( + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 15), + margin: const EdgeInsets.only(top: 15, bottom: 10), + child: Text( + widget.getTitle(), + style: (widget.hasSmallTitle + ? Theme.of(context).textTheme.titleLarge! + : Theme.of(context) + .textTheme + .headlineSmall!) + .copyWith( + color: Theme.of(context).primaryColor, ), - if (widget.editingMode) getDeleteIcon(context) - ], + ), + ), ), - Container( - padding: EdgeInsets.only( - left: padding, - right: padding, - bottom: padding, + if (widget.editingMode) + Container( + alignment: Alignment.center, + margin: const EdgeInsets.only(top: 8), + child: getMoveIcon(context), ), - child: widget.buildCardContent(context), - ) + if (widget.editingMode) getDeleteIcon(context) ], ), - ), + Container( + padding: EdgeInsets.only( + left: padding, + right: padding, + bottom: padding, + ), + child: widget.buildCardContent(context), + ) + ], ), - ))); + ), + ), + ), + ), + ); } - Widget getDeleteIcon(context) { + 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, + child: Container( + alignment: Alignment.centerRight, + height: 32, + child: IconButton( + iconSize: 22, + icon: const Icon(Icons.delete), + tooltip: 'Remover', + onPressed: widget.onDelete, + ), ), - )); + ); } - Widget getMoveIcon(context) { - return Icon(Icons.drag_handle_rounded, - color: Colors.grey.shade500, size: 22.0); + Widget getMoveIcon(BuildContext context) { + return Icon( + Icons.drag_handle_rounded, + color: Colors.grey.shade500, + size: 22, + ); } } diff --git a/uni/lib/view/common_widgets/generic_expansion_card.dart b/uni/lib/view/common_widgets/generic_expansion_card.dart index d26f3a631..57702be05 100644 --- a/uni/lib/view/common_widgets/generic_expansion_card.dart +++ b/uni/lib/view/common_widgets/generic_expansion_card.dart @@ -3,17 +3,18 @@ import 'package:flutter/material.dart'; /// Card with an expandable child abstract class GenericExpansionCard extends StatelessWidget { + const GenericExpansionCard({ + super.key, + this.smallTitle = false, + this.cardMargin, + }); final bool smallTitle; final EdgeInsetsGeometry? cardMargin; - const GenericExpansionCard( - {Key? key, this.smallTitle = false, this.cardMargin}) - : super(key: key); - - TextStyle? getTitleStyle(BuildContext context) => Theme.of(context) - .textTheme - .headlineSmall - ?.apply(color: Theme.of(context).primaryColor); + TextStyle? getTitleStyle(BuildContext context) => + Theme.of(context).textTheme.headlineSmall?.apply( + color: Theme.of(context).primaryColor, + ); String getTitle(); @@ -22,26 +23,28 @@ abstract class GenericExpansionCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: cardMargin ?? const EdgeInsets.fromLTRB(20, 10, 20, 0), - child: ExpansionTileCard( - expandedTextColor: Theme.of(context).primaryColor, - heightFactorCurve: Curves.ease, - turnsCurve: Curves.easeOutBack, - expandedColor: (Theme.of(context).brightness == Brightness.light) - ? const Color.fromARGB(0xf, 0, 0, 0) - : const Color.fromARGB(255, 43, 43, 43), - title: Text(getTitle(), - style: Theme.of(context) - .textTheme - .headlineSmall - ?.apply(color: Theme.of(context).primaryColor)), - elevation: 0, - children: [ - Container( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), - child: buildCardContent(context), - ) - ], - )); + margin: cardMargin ?? const EdgeInsets.fromLTRB(20, 10, 20, 0), + child: ExpansionTileCard( + expandedTextColor: Theme.of(context).primaryColor, + heightFactorCurve: Curves.ease, + turnsCurve: Curves.easeOutBack, + expandedColor: (Theme.of(context).brightness == Brightness.light) + ? const Color.fromARGB(0xf, 0, 0, 0) + : const Color.fromARGB(255, 43, 43, 43), + title: Text( + getTitle(), + style: Theme.of(context).textTheme.headlineSmall?.apply( + color: Theme.of(context).primaryColor, + ), + ), + elevation: 0, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + child: buildCardContent(context), + ) + ], + ), + ); } } diff --git a/uni/lib/view/common_widgets/last_update_timestamp.dart b/uni/lib/view/common_widgets/last_update_timestamp.dart index b674d1f05..4f4820b17 100644 --- a/uni/lib/view/common_widgets/last_update_timestamp.dart +++ b/uni/lib/view/common_widgets/last_update_timestamp.dart @@ -22,42 +22,45 @@ class _LastUpdateTimeStampState void initState() { super.initState(); Timer.periodic( - const Duration(seconds: 60), - (timer) => { - if (mounted) - { - setState(() { - currentTime = DateTime.now(); - }) - } - }); + const Duration(seconds: 60), + (timer) => { + if (mounted) + { + setState(() { + currentTime = DateTime.now(); + }) + } + }, + ); } @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, provider) => Container( - padding: const EdgeInsets.only(top: 8.0, bottom: 10.0), - child: provider.lastUpdateTime != null - ? _getContent(context, provider.lastUpdateTime!) - : null, - )); + builder: (context, provider) => Container( + padding: const EdgeInsets.only(top: 8, bottom: 10), + child: provider.lastUpdateTime != null + ? _getContent(context, provider.lastUpdateTime!) + : null, + ), + ); } Widget _getContent(BuildContext context, DateTime lastUpdateTime) { - final Duration elapsedTime = currentTime.difference(lastUpdateTime); - int elapsedTimeMinutes = elapsedTime.inMinutes; + final elapsedTime = currentTime.difference(lastUpdateTime); + var elapsedTimeMinutes = elapsedTime.inMinutes; if (elapsedTimeMinutes < 0) { elapsedTimeMinutes = 0; } return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Atualizado há $elapsedTimeMinutes minuto${elapsedTimeMinutes != 1 ? 's' : ''}', - style: Theme.of(context).textTheme.titleSmall) - ]); + children: [ + Text( + 'Atualizado há $elapsedTimeMinutes ' + 'minuto${elapsedTimeMinutes != 1 ? 's' : ''}', + style: Theme.of(context).textTheme.titleSmall, + ) + ], + ); } } diff --git a/uni/lib/view/common_widgets/page_title.dart b/uni/lib/view/common_widgets/page_title.dart index 7cebe9f0a..af5bbeccd 100644 --- a/uni/lib/view/common_widgets/page_title.dart +++ b/uni/lib/view/common_widgets/page_title.dart @@ -2,20 +2,23 @@ import 'package:flutter/material.dart'; /// Generic implementation of a page title class PageTitle extends StatelessWidget { + const PageTitle({ + required this.name, + super.key, + this.center = true, + this.pad = true, + }); final String name; final bool center; final bool pad; - const PageTitle( - {Key? key, required this.name, this.center = true, this.pad = true}) - : super(key: key); - @override Widget build(BuildContext context) { final Widget title = Text( name, style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).primaryTextTheme.headlineMedium?.color), + color: Theme.of(context).primaryTextTheme.headlineMedium?.color, + ), ); return Container( padding: pad ? const EdgeInsets.fromLTRB(20, 20, 20, 10) : null, diff --git a/uni/lib/view/common_widgets/page_transition.dart b/uni/lib/view/common_widgets/page_transition.dart index 0f69420b2..033d96f65 100644 --- a/uni/lib/view/common_widgets/page_transition.dart +++ b/uni/lib/view/common_widgets/page_transition.dart @@ -4,22 +4,30 @@ import 'package:flutter/material.dart'; class PageTransition { static const int pageTransitionDuration = 200; - static Route makePageTransition( - {required Widget page, - bool maintainState = true, - required RouteSettings settings}) { + static Route makePageTransition({ + required Widget page, + required RouteSettings settings, + bool maintainState = true, + }) { return PageRouteBuilder( - pageBuilder: (BuildContext context, Animation animation, - Animation secondaryAnimation) { - return page; - }, - transitionDuration: - const Duration(milliseconds: pageTransitionDuration), - settings: settings, - maintainState: maintainState, - transitionsBuilder: (BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { - return FadeTransition(opacity: animation, child: child); - }); + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return page; + }, + transitionDuration: const Duration(milliseconds: pageTransitionDuration), + settings: settings, + maintainState: maintainState, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition(opacity: animation, child: child); + }, + ); } } 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 1646fe4ba..443a8c752 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/general.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/general.dart @@ -12,7 +12,7 @@ import 'package:uni/view/profile/profile.dart'; /// Page with a hamburger menu and the user profile picture abstract class GeneralPageViewState extends State { - final double borderMargin = 18.0; + final double borderMargin = 18; static ImageProvider? profileImageProvider; Future onRefresh(BuildContext context); @@ -29,12 +29,15 @@ abstract class GeneralPageViewState extends State { return Container(); } - Future buildProfileDecorationImage(context, - {forceRetrieval = false}) async { + Future buildProfileDecorationImage( + BuildContext context, { + bool forceRetrieval = false, + }) async { final profilePictureFile = await ProfileProvider.fetchOrGetCachedProfilePicture( - null, Provider.of(context, listen: false).session, - forceRetrieval: forceRetrieval || profileImageProvider == null); + Provider.of(context, listen: false).session, + forceRetrieval: forceRetrieval || profileImageProvider == null, + ); return getProfileDecorationImage(profilePictureFile); } @@ -44,7 +47,7 @@ abstract class GeneralPageViewState extends State { DecorationImage getProfileDecorationImage(File? profilePicture) { final fallbackPicture = profileImageProvider ?? const AssetImage('assets/images/profile_placeholder.png'); - final ImageProvider image = + final image = profilePicture == null ? fallbackPicture : FileImage(profilePicture); final result = DecorationImage(fit: BoxFit.cover, image: image); @@ -57,10 +60,10 @@ abstract class GeneralPageViewState extends State { Widget refreshState(BuildContext context, Widget child) { return RefreshIndicator( key: GlobalKey(), - onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture(null, - Provider.of(context, listen: false).session, - forceRetrieval: true) - .then((value) => onRefresh(context)), + onRefresh: () => ProfileProvider.fetchOrGetCachedProfilePicture( + Provider.of(context, listen: false).session, + forceRetrieval: true, + ).then((value) => onRefresh(context)), child: child, ); } @@ -78,7 +81,7 @@ abstract class GeneralPageViewState extends State { /// 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 MediaQueryData queryData = MediaQuery.of(context); + final queryData = MediaQuery.of(context); return AppBar( bottom: PreferredSize( @@ -92,25 +95,30 @@ abstract class GeneralPageViewState extends State { elevation: 0, iconTheme: Theme.of(context).iconTheme, backgroundColor: Theme.of(context).scaffoldBackgroundColor, - titleSpacing: 0.0, + 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, + 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), ], @@ -120,20 +128,30 @@ abstract class GeneralPageViewState extends State { // 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.0, - height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, image: decorationImage.data)), - ); - }); + 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, + ), + ), + ); + }, + ); } } diff --git a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart index 192ad6f43..e8878acf6 100644 --- a/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart +++ b/uni/lib/view/common_widgets/pages_layouts/general/widgets/navigation_drawer.dart @@ -5,10 +5,9 @@ import 'package:uni/utils/drawer_items.dart'; import 'package:uni/view/theme_notifier.dart'; class AppNavigationDrawer extends StatefulWidget { + const AppNavigationDrawer({required this.parentContext, super.key}); final BuildContext parentContext; - const AppNavigationDrawer({super.key, required this.parentContext}); - @override State createState() { return AppNavigationDrawerState(); @@ -18,25 +17,25 @@ class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawerState extends State { AppNavigationDrawerState(); - Map drawerItems = {}; + Map drawerItems = {}; @override void initState() { super.initState(); drawerItems = {}; - for (var element in DrawerItem.values) { + for (final element in DrawerItem.values) { drawerItems[element] = _onSelectPage; } } // Callback Functions - getCurrentRoute() => + String getCurrentRoute() => ModalRoute.of(widget.parentContext)!.settings.name == null ? drawerItems.keys.toList()[0].title : ModalRoute.of(widget.parentContext)!.settings.name!.substring(1); - _onSelectPage(String key) { + void _onSelectPage(String key) { final prev = getCurrentRoute(); Navigator.of(context).pop(); @@ -46,7 +45,7 @@ class AppNavigationDrawerState extends State { } } - _onLogOut(String key) { + void _onLogOut(String key) { Navigator.of(context) .pushNamedAndRemoveUntil('/$key', (Route route) => false); } @@ -57,28 +56,33 @@ class AppNavigationDrawerState extends State { return (name == getCurrentRoute()) ? BoxDecoration( border: Border( - left: BorderSide( - color: Theme.of(context).primaryColor, width: 3.0)), + left: BorderSide( + color: Theme.of(context).primaryColor, + width: 3, + ), + ), color: Theme.of(context).dividerColor, ) : null; } Widget createLogoutBtn() { - final String logOutText = DrawerItem.navLogOut.title; + final logOutText = DrawerItem.navLogOut.title; return TextButton( onPressed: () => _onLogOut(logOutText), style: TextButton.styleFrom( elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 5.0), + padding: const EdgeInsets.symmetric(horizontal: 5), ), child: Container( - padding: const EdgeInsets.all(15.0), - child: Text(logOutText, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).primaryColor)), + padding: const EdgeInsets.all(15), + child: Text( + logOutText, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).primaryColor), + ), ), ); } @@ -90,7 +94,7 @@ class AppNavigationDrawerState extends State { return const Icon(Icons.wb_sunny); case ThemeMode.dark: return const Icon(Icons.nightlight_round); - default: + case ThemeMode.system: return const Icon(Icons.brightness_6); } } @@ -98,57 +102,66 @@ class AppNavigationDrawerState extends State { return Consumer( builder: (context, themeNotifier, _) { return IconButton( - icon: getThemeIcon(themeNotifier.getTheme()), - onPressed: themeNotifier.setNextTheme); + icon: getThemeIcon(themeNotifier.getTheme()), + onPressed: themeNotifier.setNextTheme, + ); }, ); } Widget createDrawerNavigationOption(DrawerItem d) { - return Container( - decoration: _getSelectionDecoration(d.title), - child: ListTile( - title: Container( - padding: const EdgeInsets.only(bottom: 3.0, left: 20.0), - child: Text(d.title, - style: TextStyle( - fontSize: 18.0, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.normal)), + return DecoratedBox( + decoration: _getSelectionDecoration(d.title) ?? const BoxDecoration(), + child: ListTile( + title: Container( + padding: const EdgeInsets.only(bottom: 3, left: 20), + child: Text( + d.title, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.normal, + ), ), - dense: true, - contentPadding: const EdgeInsets.all(0.0), - selected: d.title == getCurrentRoute(), - onTap: () => drawerItems[d]!(d.title), - )); + ), + dense: true, + contentPadding: EdgeInsets.zero, + selected: d.title == getCurrentRoute(), + onTap: () => drawerItems[d]!(d.title), + ), + ); } @override Widget build(BuildContext context) { - final List drawerOptions = []; + final drawerOptions = []; final userSession = Provider.of(context).session; - for (var key in drawerItems.keys) { + for (final key in drawerItems.keys) { if (key.isVisible(userSession.faculties)) { drawerOptions.add(createDrawerNavigationOption(key)); } } return Drawer( - child: Column( - children: [ - Expanded( + child: Column( + children: [ + Expanded( child: Container( - padding: const EdgeInsets.only(top: 55.0), - child: ListView( - children: drawerOptions, + padding: const EdgeInsets.only(top: 55), + child: ListView( + children: drawerOptions, + ), + ), ), - )), - Row(children: [ - Expanded(child: createLogoutBtn()), - createThemeSwitchBtn() - ]) - ], - )); + Row( + children: [ + Expanded(child: createLogoutBtn()), + createThemeSwitchBtn() + ], + ) + ], + ), + ); } } 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 f23e5316d..473fb3299 100644 --- a/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart +++ b/uni/lib/view/common_widgets/pages_layouts/secondary/secondary.dart @@ -5,7 +5,7 @@ import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; abstract class SecondaryPageViewState extends GeneralPageViewState { @override - getScaffold(BuildContext context, Widget body) { + Scaffold getScaffold(BuildContext context, Widget body) { return Scaffold( appBar: buildAppBar(context), body: refreshState(context, body), diff --git a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart index c1e893c2f..170203fdb 100644 --- a/uni/lib/view/common_widgets/request_dependent_widget_builder.dart +++ b/uni/lib/view/common_widgets/request_dependent_widget_builder.dart @@ -8,14 +8,14 @@ import 'package:uni/utils/drawer_items.dart'; /// hydrating the component, displaying an empty message, /// a connection error or a loading circular effect as appropriate class RequestDependentWidgetBuilder extends StatelessWidget { - const RequestDependentWidgetBuilder( - {Key? key, - required this.status, - required this.builder, - required this.hasContentPredicate, - required this.onNullContent, - this.contentLoadingWidget}) - : super(key: key); + 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; @@ -35,49 +35,72 @@ class RequestDependentWidgetBuilder extends StatelessWidget { ? builder() : Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: onNullContent); + child: onNullContent, + ); } Widget loadingWidget(BuildContext context) { return contentLoadingWidget == null ? const Center( child: Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: CircularProgressIndicator())) + padding: EdgeInsets.symmetric(vertical: 20), + child: CircularProgressIndicator(), + ), + ) : Center( child: Shimmer.fromColors( - baseColor: Theme.of(context).highlightColor, - highlightColor: Theme.of(context).colorScheme.onPrimary, - child: contentLoadingWidget!)); + 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()); - } + 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('Sem ligação à internet', - style: Theme.of(context).textTheme.titleMedium)); - } + if (connectivitySnapshot.data == ConnectivityResult.none) { + return Center( + heightFactor: 3, + child: Text( + 'Sem ligação à internet', + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } - return Column(children: [ + return Column( + children: [ Padding( - padding: const EdgeInsets.only(top: 15, bottom: 10), - child: Center( - child: Text('Aconteceu um erro ao carregar os dados', - style: Theme.of(context).textTheme.titleMedium))), + padding: const EdgeInsets.only(top: 15, bottom: 10), + child: Center( + child: Text( + 'Aconteceu um erro ao carregar os dados', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), OutlinedButton( - onPressed: () => Navigator.pushNamed( - context, '/${DrawerItem.navBugReport.title}'), - child: const Text('Reportar erro')) - ]); - }); + onPressed: () => Navigator.pushNamed( + context, + '/${DrawerItem.navBugReport.title}', + ), + child: const Text('Reportar erro'), + ) + ], + ); + }, + ); } } diff --git a/uni/lib/view/common_widgets/row_container.dart b/uni/lib/view/common_widgets/row_container.dart index 978d685fe..1e6e0cb78 100644 --- a/uni/lib/view/common_widgets/row_container.dart +++ b/uni/lib/view/common_widgets/row_container.dart @@ -2,21 +2,27 @@ import 'package:flutter/material.dart'; /// App default container class RowContainer extends StatelessWidget { + const RowContainer({ + required this.child, + super.key, + this.borderColor, + this.color, + }); final Widget child; final Color? borderColor; final Color? color; - const RowContainer( - {Key? key, required this.child, this.borderColor, this.color}) - : super(key: key); @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( - border: Border.all( - color: borderColor ?? Theme.of(context).dividerColor, width: 0.5), - color: color, - borderRadius: const BorderRadius.all(Radius.circular(7))), + border: Border.all( + color: borderColor ?? Theme.of(context).dividerColor, + width: 0.5, + ), + color: color, + borderRadius: const BorderRadius.all(Radius.circular(7)), + ), child: child, ); } diff --git a/uni/lib/view/common_widgets/toast_message.dart b/uni/lib/view/common_widgets/toast_message.dart index 65854de38..aa1dc390d 100644 --- a/uni/lib/view/common_widgets/toast_message.dart +++ b/uni/lib/view/common_widgets/toast_message.dart @@ -1,28 +1,27 @@ -import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; /// Provides feedback about an operation in a small popup /// /// usage example: ToastMessage.display(context, toastMsg); class MessageToast extends StatelessWidget { + const MessageToast({ + required this.message, + required this.icon, + super.key, + this.color = Colors.white, + this.iconColor = Colors.black, + this.textColor = Colors.black, + this.alignment = Alignment.bottomCenter, + this.elevation = 0.0, + }); final String message; final Color? color; final IconData? icon; final Color? iconColor; final Color? textColor; final AlignmentGeometry? alignment; - final dynamic elevation; - - const MessageToast( - {Key? key, - required this.message, - this.color = Colors.white, - required this.icon, - this.iconColor = Colors.black, - this.textColor = Colors.black, - this.alignment = Alignment.bottomCenter, - this.elevation = 0.0}) - : super(key: key); + final double elevation; @override Widget build(BuildContext context) { @@ -33,18 +32,20 @@ class MessageToast extends StatelessWidget { alignment: alignment, backgroundColor: color, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0))), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), elevation: elevation, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - margin: const EdgeInsets.all(10.0), - child: Icon( - icon, - color: iconColor, - )), + margin: const EdgeInsets.all(10), + child: Icon( + icon, + color: iconColor, + ), + ), Expanded( child: Text( message, @@ -61,57 +62,68 @@ class MessageToast extends StatelessWidget { } class ToastMessage { - static const Color toastErrorIconColor = Color.fromARGB(255, 241, 77, 98), - toastErrorColor = Color.fromARGB(255, 252, 237, 238), - toastSuccessIconColor = Color.fromARGB(255, 53, 210, 157), - toastSuccessColor = Color.fromARGB(255, 234, 250, 246), - toastWarningIconColor = Color.fromARGB(255, 244, 200, 98), - toastWarningColor = Color.fromARGB(255, 252, 244, 222), - toastInfoIconColor = Color.fromARGB(255, 40, 131, 229), - toastInfoColor = Color.fromARGB(255, 211, 229, 249); + static const Color toastErrorIconColor = Color.fromARGB(255, 241, 77, 98); + static const Color toastErrorColor = Color.fromARGB(255, 252, 237, 238); + static const Color toastSuccessIconColor = Color.fromARGB(255, 53, 210, 157); + static const Color toastSuccessColor = Color.fromARGB(255, 234, 250, 246); + static const Color toastWarningIconColor = Color.fromARGB(255, 244, 200, 98); + static const Color toastWarningColor = Color.fromARGB(255, 252, 244, 222); + static const Color toastInfoIconColor = Color.fromARGB(255, 40, 131, 229); + static const Color toastInfoColor = Color.fromARGB(255, 211, 229, 249); - static Future _displayDialog(BuildContext context, Widget mToast) { + static Future _displayDialog(BuildContext context, Widget mToast) { return showDialog( - barrierDismissible: false, - barrierColor: Colors.white.withOpacity(0), - context: context, - builder: (toastContext) { - Future.delayed(const Duration(milliseconds: 2000), () { - Navigator.of(toastContext).pop(); - }); - return mToast; + barrierDismissible: false, + barrierColor: Colors.white.withOpacity(0), + context: context, + builder: (toastContext) { + Future.delayed(const Duration(milliseconds: 2000), () { + Navigator.of(toastContext).pop(); }); + return mToast; + }, + ); } - static error(BuildContext context, String msg) => _displayDialog( - context, - MessageToast( + static Future error(BuildContext context, String msg) => _displayDialog( + context, + MessageToast( message: msg, color: toastErrorColor, icon: CupertinoIcons.clear_circled_solid, - iconColor: toastErrorIconColor)); + iconColor: toastErrorIconColor, + ), + ); - static success(BuildContext context, String msg) => _displayDialog( - context, - MessageToast( + static Future success(BuildContext context, String msg) => + _displayDialog( + context, + MessageToast( message: msg, color: toastSuccessColor, icon: CupertinoIcons.check_mark_circled_solid, - iconColor: toastSuccessIconColor)); + iconColor: toastSuccessIconColor, + ), + ); - static warning(BuildContext context, String msg) => _displayDialog( - context, - MessageToast( + static Future warning(BuildContext context, String msg) => + _displayDialog( + context, + MessageToast( message: msg, color: toastWarningColor, icon: CupertinoIcons.exclamationmark_circle_fill, - iconColor: toastWarningIconColor)); + iconColor: toastWarningIconColor, + ), + ); - static info(BuildContext context, String msg) => _displayDialog( - context, - MessageToast( + static Future info(BuildContext context, String msg) => _displayDialog( + context, + MessageToast( message: msg, color: toastInfoColor, icon: CupertinoIcons.info_circle_fill, - iconColor: toastInfoIconColor)); + iconColor: toastInfoIconColor, + ), + ); } 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 b3f40b3e0..7c0d61596 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; -import 'package:uni/model/entities/course_units/course_unit_class.dart'; -import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/providers/lazy/course_units_info_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/page_title.dart'; @@ -13,10 +11,9 @@ import 'package:uni/view/course_unit_info/widgets/course_unit_sheet.dart'; import 'package:uni/view/lazy_consumer.dart'; class CourseUnitDetailPageView extends StatefulWidget { + const CourseUnitDetailPageView(this.courseUnit, {super.key}); final CourseUnit courseUnit; - const CourseUnitDetailPageView(this.courseUnit, {Key? key}) : super(key: key); - @override State createState() { return CourseUnitDetailPageViewState(); @@ -25,45 +22,53 @@ class CourseUnitDetailPageView extends StatefulWidget { class CourseUnitDetailPageViewState extends SecondaryPageViewState { - Future loadInfo(bool force) async { + Future loadInfo({required bool force}) async { final courseUnitsProvider = Provider.of(context, listen: false); final session = context.read().session; - final CourseUnitSheet? courseUnitSheet = + final courseUnitSheet = courseUnitsProvider.courseUnitsSheets[widget.courseUnit]; if (courseUnitSheet == null || force) { - courseUnitsProvider.fetchCourseUnitSheet(widget.courseUnit, session); + await courseUnitsProvider.fetchCourseUnitSheet( + widget.courseUnit, + session, + ); } - final List? courseUnitClasses = + final courseUnitClasses = courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; if (courseUnitClasses == null || force) { - courseUnitsProvider.fetchCourseUnitClasses(widget.courseUnit, session); + await courseUnitsProvider.fetchCourseUnitClasses( + widget.courseUnit, + session, + ); } } @override Future onRefresh(BuildContext context) async { - loadInfo(true); + await loadInfo(force: true); } @override Future onLoad(BuildContext context) async { - loadInfo(false); + await loadInfo(force: false); } @override Widget getBody(BuildContext context) { return DefaultTabController( - length: 2, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + length: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ PageTitle( center: false, name: widget.courseUnit.name, ), const TabBar( - tabs: [Tab(text: "Ficha"), Tab(text: "Turmas")], + tabs: [Tab(text: 'Ficha'), Tab(text: 'Turmas')], ), Expanded( child: Padding( @@ -76,34 +81,56 @@ class CourseUnitDetailPageViewState ), ), ) - ])); + ], + ), + ); } Widget _courseUnitSheetView(BuildContext context) { return LazyConsumer( - builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), + builder: (context, courseUnitsInfoProvider) { + return RequestDependentWidgetBuilder( + onNullContent: const Center( + child: Text( + 'Não existem informações para apresentar', + textAlign: TextAlign.center, + ), + ), status: courseUnitsInfoProvider.status, builder: () => CourseUnitSheetView( - courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]!), + courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]!, + ), hasContentPredicate: courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit] != - null); - }); + null && + courseUnitsInfoProvider.courseUnitsSheets[widget.courseUnit]! + .sections.isNotEmpty, + ); + }, + ); } Widget _courseUnitClassesView(BuildContext context) { return LazyConsumer( - builder: (context, courseUnitsInfoProvider) { - return RequestDependentWidgetBuilder( - onNullContent: const Center(), + builder: (context, courseUnitsInfoProvider) { + return RequestDependentWidgetBuilder( + onNullContent: const Center( + child: Text( + 'Não existem turmas para apresentar', + textAlign: TextAlign.center, + ), + ), status: courseUnitsInfoProvider.status, builder: () => CourseUnitClassesView( - courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit]!), + courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit]!, + ), hasContentPredicate: courseUnitsInfoProvider.courseUnitsClasses[widget.courseUnit] != - null); - }); + null && + courseUnitsInfoProvider + .courseUnitsClasses[widget.courseUnit]!.isNotEmpty, + ); + }, + ); } } 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 211843c1d..afd997326 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -1,28 +1,31 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; -import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_student_row.dart'; class CourseUnitClassesView extends StatelessWidget { - final List classes; - const CourseUnitClassesView(this.classes, {super.key}); + final List classes; @override Widget build(BuildContext context) { - final Session session = context.read().session; - final List cards = []; - for (CourseUnitClass courseUnitClass in classes) { - final bool isMyClass = courseUnitClass.students - .where((student) => - student.number == - (int.tryParse(session.username.replaceAll(RegExp(r"\D"), "")) ?? - 0)) + final session = context.read().session; + final cards = []; + for (final courseUnitClass in classes) { + final isMyClass = courseUnitClass.students + .where( + (student) => + student.number == + (int.tryParse( + session.username.replaceAll(RegExp(r'\D'), ''), + ) ?? + 0), + ) .isNotEmpty; - cards.add(CourseUnitInfoCard( + cards.add( + CourseUnitInfoCard( isMyClass ? '${courseUnitClass.className} *' : courseUnitClass.className, @@ -30,11 +33,14 @@ class CourseUnitClassesView extends StatelessWidget { children: courseUnitClass.students .map((student) => CourseUnitStudentRow(student, session)) .toList(), - ))); + ), + ), + ); } return Container( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ListView(children: cards)); + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards), + ); } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart index 3295b04cd..389e36ad8 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_info_card.dart @@ -2,15 +2,14 @@ import 'package:flutter/material.dart'; import 'package:uni/view/common_widgets/generic_expansion_card.dart'; class CourseUnitInfoCard extends GenericExpansionCard { + const CourseUnitInfoCard(this.sectionTitle, this.content, {super.key}) + : super( + cardMargin: const EdgeInsets.only(bottom: 10), + smallTitle: true, + ); final String sectionTitle; final Widget content; - const CourseUnitInfoCard(this.sectionTitle, this.content, {key}) - : super( - key: key, - cardMargin: const EdgeInsets.only(bottom: 10), - smallTitle: true); - @override Widget buildCardContent(BuildContext context) { return Container(padding: const EdgeInsets.only(top: 10), child: content); diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart index 133100a7d..e113055f3 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_sheet.dart @@ -3,77 +3,76 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; import 'package:html/dom.dart' as dom; -import 'package:provider/provider.dart'; -import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; -import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; class CourseUnitSheetView extends StatelessWidget { - final CourseUnitSheet courseUnitSheet; - const CourseUnitSheetView(this.courseUnitSheet, {super.key}); + final CourseUnitSheet courseUnitSheet; @override Widget build(BuildContext context) { - final session = context.read().session; - final baseUrl = Uri.parse(NetworkRouter.getBaseUrl(session.faculties[0])); - - final List cards = []; - for (var section in courseUnitSheet.sections.entries) { - cards.add(_buildCard(section.key, section.value, baseUrl)); - } - return Container( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ListView(children: cards)); + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView( + children: courseUnitSheet.sections.entries + .map((e) => _buildCard(e.key, e.value)) + .toList(), + ), + ); } CourseUnitInfoCard _buildCard( - String sectionTitle, String sectionContent, Uri baseUrl) { + String sectionTitle, + String sectionContent, + ) { return CourseUnitInfoCard( - sectionTitle, - HtmlWidget( - sectionContent, - renderMode: RenderMode.column, - baseUrl: baseUrl, - customWidgetBuilder: (element) { - if (element.className == "informa" || - element.className == "limpar") { - return Container(); - } - if (element.localName == 'table') { - try { - element = _preprocessTable(element); - final tBody = element.children - .firstWhere((element) => element.localName == 'tbody'); - final rows = tBody.children; - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Table( - border: TableBorder.all(), - children: rows - .map((e) => TableRow( - children: e.children - .sublist(0, min(4, e.children.length)) - .map((e) => TableCell( - child: Padding( - padding: const EdgeInsets.all(8), - child: HtmlWidget( - e.outerHtml, - renderMode: RenderMode.column, - baseUrl: baseUrl, - )))) - .toList())) - .toList(), - )); - } catch (e) { - return null; - } + sectionTitle, + HtmlWidget( + sectionContent, + customWidgetBuilder: (element) { + if (element.className == 'informa' || element.className == 'limpar') { + return Container(); + } + if (element.localName == 'table') { + try { + element = _preprocessTable(element); + final tBody = element.children + .firstWhere((element) => element.localName == 'tbody'); + final rows = tBody.children; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Table( + border: TableBorder.all(), + children: rows + .map( + (e) => TableRow( + children: e.children + .sublist(0, min(4, e.children.length)) + .map( + (e) => TableCell( + child: Padding( + padding: const EdgeInsets.all(8), + child: HtmlWidget( + e.outerHtml, + ), + ), + ), + ) + .toList(), + ), + ) + .toList(), + ), + ); + } catch (e) { + return null; } - return null; - }, - )); + } + return null; + }, + ), + ); } dom.Element _preprocessTable(dom.Element tableElement) { @@ -82,16 +81,16 @@ class CourseUnitSheetView extends StatelessWidget { .firstWhere((element) => element.localName == 'tbody'); final rows = tBody.children; - for (int i = 0; i < rows.length; i++) { - for (int j = 0; j < rows[i].children.length; j++) { + for (var i = 0; i < rows.length; i++) { + for (var j = 0; j < rows[i].children.length; j++) { final cell = rows[i].children[j]; if (cell.attributes['rowspan'] != null) { final rowSpan = int.parse(cell.attributes['rowspan']!); if (rowSpan <= 1) { continue; } - processedTable.children[0].children[i].children[j].innerHtml = ""; - for (int k = 1; k < rowSpan; k++) { + processedTable.children[0].children[i].children[j].innerHtml = ''; + for (var k = 1; k < rowSpan; k++) { try { processedTable.children[0].children[i + k].children .insert(j, cell.clone(true)); diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart index 74b17ae3a..c0e7a6f95 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_student_row.dart @@ -13,43 +13,55 @@ class CourseUnitStudentRow extends StatelessWidget { @override Widget build(BuildContext context) { - final Future userImage = - ProfileProvider.fetchOrGetCachedProfilePicture(student.number, session); + final userImage = ProfileProvider.fetchOrGetCachedProfilePicture( + session, + studentNumber: student.number, + ); return FutureBuilder( builder: (BuildContext context, AsyncSnapshot snapshot) { return Container( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.cover, - image: snapshot.hasData && - snapshot.data!.lengthSync() > 0 - ? FileImage(snapshot.data!) as ImageProvider - : const AssetImage( - 'assets/images/profile_placeholder.png')))), - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(student.name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge), - Opacity( - opacity: 0.8, - child: Text( - "up${student.number}", - )) - ]))) - ], - )); + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: snapshot.hasData && snapshot.data!.lengthSync() > 0 + ? FileImage(snapshot.data!) as ImageProvider + : const AssetImage( + 'assets/images/profile_placeholder.png', + ), + ), + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + student.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + Opacity( + opacity: 0.8, + child: Text( + 'up${student.number}', + ), + ) + ], + ), + ), + ) + ], + ), + ); }, future: userImage, ); diff --git a/uni/lib/view/course_units/course_units.dart b/uni/lib/view/course_units/course_units.dart index 790a4e894..e59b22150 100644 --- a/uni/lib/view/course_units/course_units.dart +++ b/uni/lib/view/course_units/course_units.dart @@ -12,7 +12,7 @@ import 'package:uni/view/course_units/widgets/course_unit_card.dart'; import 'package:uni/view/lazy_consumer.dart'; class CourseUnitsPageView extends StatefulWidget { - const CourseUnitsPageView({Key? key}) : super(key: key); + const CourseUnitsPageView({super.key}); static const bothSemestersDropdownOption = '1S+2S'; @@ -29,145 +29,175 @@ class CourseUnitsPageViewState @override Widget getBody(BuildContext context) { - return LazyConsumer(builder: (context, profileProvider) { - final List courseUnits = profileProvider.profile.courseUnits; - List availableYears = []; - List availableSemesters = []; - - if (courseUnits.isNotEmpty) { - availableYears = _getAvailableYears(courseUnits); - if (availableYears.isNotEmpty && selectedSchoolYear == null) { - selectedSchoolYear = availableYears.reduce((value, element) => - element.compareTo(value) > 0 ? element : value); - } - availableSemesters = _getAvailableSemesters(courseUnits); - final currentYear = int.tryParse(selectedSchoolYear?.substring( - 0, selectedSchoolYear?.indexOf('/')) ?? - ''); - if (selectedSemester == null && - currentYear != null && - availableSemesters.length == 3) { - final currentDate = DateTime.now(); - selectedSemester = - currentDate.year <= currentYear || currentDate.month == 1 - ? availableSemesters[0] - : availableSemesters[1]; + return LazyConsumer( + builder: (context, profileProvider) { + final courseUnits = profileProvider.profile.courseUnits; + var availableYears = []; + var availableSemesters = []; + + if (courseUnits.isNotEmpty) { + availableYears = _getAvailableYears(courseUnits); + if (availableYears.isNotEmpty && selectedSchoolYear == null) { + selectedSchoolYear = availableYears.reduce( + (value, element) => + element.compareTo(value) > 0 ? element : value, + ); + } + + availableSemesters = _getAvailableSemesters(courseUnits); + + if (availableSemesters.length == 3 && selectedSemester == null) { + selectedSemester = availableSemesters[2]; + } } - } - return _getPageView(courseUnits, profileProvider.status, availableYears, - availableSemesters); - }); + return _getPageView( + courseUnits, + profileProvider.status, + availableYears, + availableSemesters, + ); + }, + ); } Widget _getPageView( - List? courseUnits, - RequestStatus requestStatus, - List availableYears, - List availableSemesters) { - final List? filteredCourseUnits = + List courseUnits, + RequestStatus requestStatus, + List availableYears, + List availableSemesters, + ) { + final filteredCourseUnits = selectedSemester == CourseUnitsPageView.bothSemestersDropdownOption ? courseUnits - ?.where((element) => element.schoolYear == selectedSchoolYear) + .where((element) => element.schoolYear == selectedSchoolYear) .toList() : courseUnits - ?.where((element) => - element.schoolYear == selectedSchoolYear && - element.semesterCode == selectedSemester) + .where( + (element) => + element.schoolYear == selectedSchoolYear && + element.semesterCode == selectedSemester, + ) .toList(); - return Column(children: [ - _getPageTitleAndFilters(availableYears, availableSemesters), - RequestDependentWidgetBuilder( + return Column( + children: [ + _getPageTitleAndFilters(availableYears, availableSemesters), + RequestDependentWidgetBuilder( status: requestStatus, builder: () => _generateCourseUnitsCards(filteredCourseUnits, context), - hasContentPredicate: courseUnits?.isNotEmpty ?? false, + hasContentPredicate: courseUnits.isNotEmpty, onNullContent: Center( heightFactor: 10, - child: Text('Não existem cadeiras para apresentar', - style: Theme.of(context).textTheme.titleLarge), - )) - ]); + child: Text( + 'Não existem cadeiras para apresentar', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ) + ], + ); } Widget _getPageTitleAndFilters( - List availableYears, List availableSemesters) { + List availableYears, + List availableSemesters, + ) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ PageTitle(name: DrawerItem.navCourseUnits.title), const Spacer(), DropdownButtonHideUnderline( - child: DropdownButton( - alignment: AlignmentDirectional.centerEnd, - disabledHint: const Text('Semestre'), - value: selectedSemester, - icon: const Icon(Icons.arrow_drop_down), - onChanged: (String? newValue) { - setState(() => selectedSemester = newValue!); - }, - items: - availableSemesters.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - )), + child: DropdownButton( + alignment: AlignmentDirectional.centerEnd, + disabledHint: const Text('Semestre'), + value: selectedSemester, + icon: const Icon(Icons.arrow_drop_down), + onChanged: (String? newValue) { + setState(() => selectedSemester = newValue); + }, + items: availableSemesters + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), const SizedBox(width: 10), DropdownButtonHideUnderline( - child: DropdownButton( - disabledHint: const Text('Ano'), - value: selectedSchoolYear, - icon: const Icon(Icons.arrow_drop_down), - onChanged: (String? newValue) { - setState(() => selectedSchoolYear = newValue!); - }, - items: availableYears.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - )), + child: DropdownButton( + disabledHint: const Text('Ano'), + value: selectedSchoolYear, + icon: const Icon(Icons.arrow_drop_down), + onChanged: (String? newValue) { + setState(() => selectedSchoolYear = newValue); + }, + items: availableYears.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), const SizedBox(width: 20) ], ); } - Widget _generateCourseUnitsCards(courseUnits, context) { - if ((courseUnits as List).isEmpty) { + Widget _generateCourseUnitsCards( + List courseUnits, + BuildContext context, + ) { + if (courseUnits.isEmpty) { return Center( - heightFactor: 10, - child: Text('Sem cadeiras no período selecionado', - style: Theme.of(context).textTheme.titleLarge)); + heightFactor: 10, + child: Text( + 'Sem cadeiras no período selecionado', + style: Theme.of(context).textTheme.titleLarge, + ), + ); } return Expanded( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 20), - child: ListView( - shrinkWrap: true, - children: _generateCourseUnitsGridView(courseUnits), - ))); + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + child: ListView( + shrinkWrap: true, + children: _generateCourseUnitsGridView(courseUnits), + ), + ), + ); } List _generateCourseUnitsGridView(List courseUnits) { - final List rows = []; + final rows = []; for (var i = 0; i < courseUnits.length; i += 2) { if (i < courseUnits.length - 1) { - rows.add(IntrinsicHeight( - child: - Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Flexible(child: CourseUnitCard(courseUnits[i])), - const SizedBox(width: 10), - Flexible(child: CourseUnitCard(courseUnits[i + 1])), - ]))); + rows.add( + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible(child: CourseUnitCard(courseUnits[i])), + const SizedBox(width: 10), + Flexible(child: CourseUnitCard(courseUnits[i + 1])), + ], + ), + ), + ); } else { - rows.add(Row(children: [ - Flexible(child: CourseUnitCard(courseUnits[i])), - const SizedBox(width: 10), - const Spacer() - ])); + rows.add( + Row( + children: [ + Flexible(child: CourseUnitCard(courseUnits[i])), + const SizedBox(width: 10), + const Spacer() + ], + ), + ); } } return rows; 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 dee0280c9..385fed7f8 100644 --- a/uni/lib/view/course_units/widgets/course_unit_card.dart +++ b/uni/lib/view/course_units/widgets/course_unit_card.dart @@ -4,28 +4,28 @@ import 'package:uni/view/common_widgets/generic_card.dart'; import 'package:uni/view/course_unit_info/course_unit_info.dart'; class CourseUnitCard extends GenericCard { + CourseUnitCard(this.courseUnit, {super.key}) + : super.customStyle( + margin: const EdgeInsets.only(top: 10), + hasSmallTitle: true, + onDelete: () {}, + editingMode: false, + ); static const maxTitleLength = 60; final CourseUnit courseUnit; - CourseUnitCard(this.courseUnit, {Key? key}) - : super.customStyle( - key: key, - margin: const EdgeInsets.only(top: 10), - hasSmallTitle: true, - onDelete: () => null, - editingMode: false); - @override Widget buildCardContent(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), - child: Row( - children: [ - Text("${courseUnit.ects.toString().replaceAll('.0', '')} ECTS"), - const Spacer(), - Text(courseUnit.grade ?? '-') - ], - )); + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + child: Row( + children: [ + Text("${courseUnit.ects.toString().replaceAll('.0', '')} ECTS"), + const Spacer(), + Text(courseUnit.grade ?? '-') + ], + ), + ); } @override @@ -36,11 +36,13 @@ class CourseUnitCard extends GenericCard { } @override - onClick(BuildContext context) { + void onClick(BuildContext context) { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CourseUnitDetailPageView(courseUnit))); + context, + MaterialPageRoute( + builder: (context) => CourseUnitDetailPageView(courseUnit), + ), + ); } @override diff --git a/uni/lib/view/exams/exams.dart b/uni/lib/view/exams/exams.dart index 1527e3b6e..59bda1785 100644 --- a/uni/lib/view/exams/exams.dart +++ b/uni/lib/view/exams/exams.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/view/exams/widgets/day_title.dart'; -import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/exams/widgets/exam_page_title.dart'; import 'package:uni/view/exams/widgets/exam_row.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -19,42 +19,45 @@ class ExamsPageView extends StatefulWidget { /// Tracks the state of `ExamsLists`. class ExamsPageViewState extends GeneralPageViewState { - final double borderRadius = 10.0; + final double borderRadius = 10; @override Widget getBody(BuildContext context) { - return LazyConsumer(builder: (context, examProvider) { - return ListView( - children: [ - Column( - mainAxisSize: MainAxisSize.max, - children: - createExamsColumn(context, examProvider.getFilteredExams()), - ) - ], - ); - }); + return LazyConsumer( + builder: (context, examProvider) { + return ListView( + children: [ + Column( + children: + createExamsColumn(context, examProvider.getFilteredExams()), + ) + ], + ); + }, + ); } /// Creates a column with all the user's exams. - List createExamsColumn(context, List exams) { - final List columns = []; - - columns.add(const ExamPageTitle()); + List createExamsColumn(BuildContext context, List exams) { + final columns = [const ExamPageTitle()]; if (exams.isEmpty) { - columns.add(Center( + columns.add( + Center( heightFactor: 1.2, child: ImageLabel( imagePath: 'assets/images/vacation.png', label: 'Parece que estás de férias!', labelTextStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Theme.of(context).colorScheme.primary), + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), sublabel: 'Não tens exames marcados', sublabelTextStyle: const TextStyle(fontSize: 15), - ))); + ), + ), + ); return columns; } @@ -63,9 +66,9 @@ class ExamsPageViewState extends GeneralPageViewState { return columns; } - final List currentDayExams = []; + final currentDayExams = []; - for (int i = 0; i < exams.length; i++) { + for (var i = 0; i < exams.length; i++) { if (i + 1 >= exams.length) { if (exams[i].begin.day == exams[i - 1].begin.day && exams[i].begin.month == exams[i - 1].begin.month) { @@ -74,8 +77,9 @@ class ExamsPageViewState extends GeneralPageViewState { if (currentDayExams.isNotEmpty) { columns.add(createExamCard(context, currentDayExams)); } - currentDayExams.clear(); - currentDayExams.add(exams[i]); + currentDayExams + ..clear() + ..add(exams[i]); } columns.add(createExamCard(context, currentDayExams)); break; @@ -92,7 +96,7 @@ class ExamsPageViewState extends GeneralPageViewState { return columns; } - Widget createExamCard(context, exams) { + Widget createExamCard(BuildContext context, List exams) { final keyValue = exams.map((exam) => exam.toString()).join(); return Container( key: Key(keyValue), @@ -102,29 +106,33 @@ class ExamsPageViewState extends GeneralPageViewState { ); } - Widget createExamsCards(context, List exams) { - final List examCards = []; - examCards.add(DayTitle( + Widget createExamsCards(BuildContext context, List exams) { + final examCards = [ + DayTitle( day: exams[0].begin.day.toString(), weekDay: exams[0].weekDay, - month: exams[0].month)); - for (int i = 0; i < exams.length; i++) { + month: exams[0].month, + ), + ]; + for (var i = 0; i < exams.length; i++) { examCards.add(createExamContext(context, exams[i])); } return Column(children: examCards); } - Widget createExamContext(context, Exam exam) { + Widget createExamContext(BuildContext context, Exam exam) { final isHidden = Provider.of(context).hiddenExams.contains(exam.id); return Container( - key: Key('$exam-exam'), - margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), - child: RowContainer( - color: isHidden - ? Theme.of(context).hintColor - : Theme.of(context).scaffoldBackgroundColor, - child: ExamRow(exam: exam, teacher: '', mainPage: false))); + key: Key('$exam-exam'), + margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), + child: RowContainer( + color: isHidden + ? Theme.of(context).hintColor + : Theme.of(context).scaffoldBackgroundColor, + child: ExamRow(exam: exam, teacher: '', mainPage: false), + ), + ); } @override diff --git a/uni/lib/view/exams/widgets/day_title.dart b/uni/lib/view/exams/widgets/day_title.dart index 0dc07515c..56710245a 100644 --- a/uni/lib/view/exams/widgets/day_title.dart +++ b/uni/lib/view/exams/widgets/day_title.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; class DayTitle extends StatelessWidget { - final String day; - final String weekDay; - final String month; const DayTitle({ - Key? key, required this.day, required this.weekDay, required this.month, - }) : super(key: key); + super.key, + }); + final String day; + final String weekDay; + final String month; @override Widget build(BuildContext context) { diff --git a/uni/lib/view/exams/widgets/exam_filter_form.dart b/uni/lib/view/exams/widgets/exam_filter_form.dart index 15436c4f3..91a1483aa 100644 --- a/uni/lib/view/exams/widgets/exam_filter_form.dart +++ b/uni/lib/view/exams/widgets/exam_filter_form.dart @@ -4,9 +4,8 @@ import 'package:uni/model/entities/exam.dart'; import 'package:uni/model/providers/lazy/exam_provider.dart'; class ExamFilterForm extends StatefulWidget { - final Map filteredExamsTypes; - const ExamFilterForm(this.filteredExamsTypes, {super.key}); + final Map filteredExamsTypes; @override ExamFilterFormState createState() => ExamFilterFormState(); @@ -16,37 +15,44 @@ class ExamFilterFormState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Definições Filtro de Exames', - style: Theme.of(context).textTheme.headlineSmall), + title: Text( + 'Definições Filtro de Exames', + style: Theme.of(context).textTheme.headlineSmall, + ), actions: [ TextButton( - child: - Text('Cancelar', style: Theme.of(context).textTheme.bodyMedium), - onPressed: () => Navigator.pop(context)), + child: + Text('Cancelar', style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => Navigator.pop(context), + ), ElevatedButton( - child: const Text('Confirmar'), - onPressed: () { - Provider.of(context, listen: false) - .setFilteredExams(widget.filteredExamsTypes); - Navigator.pop(context); - }) + child: const Text('Confirmar'), + onPressed: () { + Provider.of(context, listen: false) + .setFilteredExams(widget.filteredExamsTypes); + Navigator.pop(context); + }, + ) ], content: SizedBox( - height: 230.0, - width: 200.0, - child: getExamCheckboxes(widget.filteredExamsTypes, context)), + height: 230, + width: 200, + child: getExamCheckboxes(widget.filteredExamsTypes, context), + ), ); } Widget getExamCheckboxes( - Map filteredExams, BuildContext context) { + Map filteredExams, + BuildContext context, + ) { filteredExams.removeWhere((key, value) => !Exam.types.containsKey(key)); return ListView( - children: List.generate(filteredExams.length, (i) { - final String key = filteredExams.keys.elementAt(i); - if (!Exam.types.containsKey(key)) return const Text(""); - return CheckboxListTile( - contentPadding: const EdgeInsets.all(0), + children: List.generate(filteredExams.length, (i) { + final key = filteredExams.keys.elementAt(i); + if (!Exam.types.containsKey(key)) return const Text(''); + return CheckboxListTile( + contentPadding: EdgeInsets.zero, title: Text( key, overflow: TextOverflow.ellipsis, @@ -59,7 +65,9 @@ class ExamFilterFormState extends State { setState(() { filteredExams[key] = value!; }); - }); - })); + }, + ); + }), + ); } } diff --git a/uni/lib/view/exams/widgets/exam_filter_menu.dart b/uni/lib/view/exams/widgets/exam_filter_menu.dart index 0576ebe67..1ea516678 100644 --- a/uni/lib/view/exams/widgets/exam_filter_menu.dart +++ b/uni/lib/view/exams/widgets/exam_filter_menu.dart @@ -12,21 +12,24 @@ class ExamFilterMenu extends StatefulWidget { class ExamFilterMenuState extends State { @override - Widget build(context) { + 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))); - }); + 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 index 32769c369..2e8592bc4 100644 --- a/uni/lib/view/exams/widgets/exam_page_title.dart +++ b/uni/lib/view/exams/widgets/exam_page_title.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:uni/view/exams/widgets/exam_filter_menu.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({Key? key}) : super(key: key); + const ExamPageTitle({super.key}); @override Widget build(BuildContext context) { diff --git a/uni/lib/view/exams/widgets/exam_row.dart b/uni/lib/view/exams/widgets/exam_row.dart index a2de2c14e..4106228f3 100644 --- a/uni/lib/view/exams/widgets/exam_row.dart +++ b/uni/lib/view/exams/widgets/exam_row.dart @@ -8,16 +8,15 @@ import 'package:uni/view/exams/widgets/exam_time.dart'; import 'package:uni/view/exams/widgets/exam_title.dart'; class ExamRow extends StatefulWidget { - final Exam exam; - final String teacher; - final bool mainPage; - const ExamRow({ - Key? key, required this.exam, required this.teacher, required this.mainPage, - }) : super(key: key); + super.key, + }); + final Exam exam; + final String teacher; + final bool mainPage; @override State createState() { @@ -31,78 +30,88 @@ class _ExamRowState extends State { final isHidden = Provider.of(context).hiddenExams.contains(widget.exam.id); final roomsKey = - '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.beginTime}-${widget.exam.endTime}'; + '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.beginTime}-' + '${widget.exam.endTime}'; return Center( - child: Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Container( - margin: const EdgeInsets.only(top: 8, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ExamTime( - begin: widget.exam.beginTime, - ) - ]), - ExamTitle( - subject: widget.exam.subject, - type: widget.exam.type), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!widget.mainPage) - IconButton( - icon: !isHidden - ? const Icon(Icons.visibility, size: 30) - : const Icon(Icons.visibility_off, - size: 30), - tooltip: isHidden - ? "Mostrar na Área Pessoal" - : "Ocultar da Área Pessoal", - onPressed: () => setState(() { - Provider.of(context, - listen: false) - .toggleHiddenExam(widget.exam.id); - })), - IconButton( - icon: Icon(MdiIcons.calendarPlus, size: 30), - onPressed: () => Add2Calendar.addEvent2Cal( - createExamEvent())), - ]), - ], - )), - Container( - key: Key(roomsKey), - alignment: Alignment.topLeft, - child: getExamRooms(context)) - ], - ))); + child: Container( + padding: const EdgeInsets.only(left: 12, bottom: 8, right: 12), + margin: const EdgeInsets.only(top: 8), + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ExamTime( + begin: widget.exam.beginTime, + ) + ], + ), + ExamTitle( + subject: widget.exam.subject, + type: widget.exam.type, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!widget.mainPage) + IconButton( + icon: !isHidden + ? const Icon(Icons.visibility, size: 30) + : const Icon( + Icons.visibility_off, + size: 30, + ), + tooltip: isHidden + ? 'Mostrar na Área Pessoal' + : 'Ocultar da Área Pessoal', + onPressed: () => setState(() { + Provider.of( + context, + listen: false, + ).toggleHiddenExam(widget.exam.id); + }), + ), + IconButton( + icon: Icon(MdiIcons.calendarPlus, size: 30), + onPressed: () => Add2Calendar.addEvent2Cal( + createExamEvent(), + ), + ), + ], + ), + ], + ), + ), + Container( + key: Key(roomsKey), + alignment: Alignment.topLeft, + child: getExamRooms(context), + ) + ], + ), + ), + ); } - Widget? getExamRooms(context) { + Widget? getExamRooms(BuildContext context) { if (widget.exam.rooms[0] == '') return null; return Wrap( - alignment: WrapAlignment.start, - spacing: 13, - children: roomsList(context, widget.exam.rooms)); + spacing: 13, + children: roomsList(context, widget.exam.rooms), + ); } - List roomsList(BuildContext context, List rooms) { + List roomsList(BuildContext context, List rooms) { return rooms - .map((room) => - Text(room.trim(), style: Theme.of(context).textTheme.bodyMedium)) + .map( + (room) => + Text(room.trim(), style: Theme.of(context).textTheme.bodyMedium), + ) .toList(); } diff --git a/uni/lib/view/exams/widgets/exam_time.dart b/uni/lib/view/exams/widgets/exam_time.dart index 884631aa4..de8705099 100644 --- a/uni/lib/view/exams/widgets/exam_time.dart +++ b/uni/lib/view/exams/widgets/exam_time.dart @@ -1,15 +1,13 @@ import 'package:flutter/material.dart'; class ExamTime extends StatelessWidget { + const ExamTime({required this.begin, super.key}); final String begin; - const ExamTime({Key? key, required this.begin}) : super(key: key); - @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, children: [ Text(begin, style: Theme.of(context).textTheme.bodyMedium), ], diff --git a/uni/lib/view/exams/widgets/exam_title.dart b/uni/lib/view/exams/widgets/exam_title.dart index 743a0a952..6b13bb68a 100644 --- a/uni/lib/view/exams/widgets/exam_title.dart +++ b/uni/lib/view/exams/widgets/exam_title.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; class ExamTitle extends StatelessWidget { + const ExamTitle({ + required this.subject, + this.type, + this.reverseOrder = false, + super.key, + }); + static const double borderRadius = 12; + static const double sideSizing = 12; final String subject; final String? type; - final double borderRadius = 12.0; - final double sideSizing = 12.0; final bool reverseOrder; - const ExamTitle( - {Key? key, required this.subject, this.type, this.reverseOrder = false}) - : super(key: key); - @override Widget build(BuildContext context) { return Container( @@ -18,18 +20,23 @@ class ExamTitle extends StatelessWidget { ); } - Widget createTopRectangle(context) { - final Text typeWidget = Text(type != null ? ' ($type) ' : '', - style: Theme.of(context).textTheme.bodyMedium); - final Text subjectWidget = Text(subject, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.apply(color: Theme.of(context).colorScheme.tertiary)); + Widget createTopRectangle(BuildContext context) { + final typeWidget = Text( + type != null ? ' ($type) ' : '', + style: Theme.of(context).textTheme.bodyMedium, + ); + final subjectWidget = Text( + subject, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.apply(color: Theme.of(context).colorScheme.tertiary), + ); return Row( - children: (reverseOrder - ? [typeWidget, subjectWidget] - : [subjectWidget, typeWidget])); + children: reverseOrder + ? [typeWidget, subjectWidget] + : [subjectWidget, typeWidget], + ); } } diff --git a/uni/lib/view/home/home.dart b/uni/lib/view/home/home.dart index 579e463ab..b5c5d9d99 100644 --- a/uni/lib/view/home/home.dart +++ b/uni/lib/view/home/home.dart @@ -22,8 +22,10 @@ class HomePageViewState extends GeneralPageViewState { Future onRefresh(BuildContext context) async { final favoriteCardTypes = context.read().favoriteCards; final cards = favoriteCardTypes - .map((e) => - MainCardsList.cardCreators[e]!(const Key(""), false, () => null)) + .map( + (e) => + MainCardsList.cardCreators[e]!(const Key(''), editingMode: false), + ) .toList(); for (final card in cards) { diff --git a/uni/lib/view/home/widgets/bus_stop_card.dart b/uni/lib/view/home/widgets/bus_stop_card.dart index 0e66d9cae..7dcdbb7db 100644 --- a/uni/lib/view/home/widgets/bus_stop_card.dart +++ b/uni/lib/view/home/widgets/bus_stop_card.dart @@ -13,14 +13,16 @@ import 'package:uni/view/lazy_consumer.dart'; /// Manages the bus stops card displayed on the user's personal area class BusStopCard extends GenericCard { const BusStopCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override String getTitle() => 'Autocarros'; @override - onClick(BuildContext context) => + Future onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navStops.title}'); @override @@ -28,7 +30,10 @@ class BusStopCard extends GenericCard { return LazyConsumer( builder: (context, busProvider) { return getCardContent( - context, busProvider.configuredBusStops, busProvider.status); + context, + busProvider.configuredBusStops, + busProvider.status, + ); }, ); } @@ -41,32 +46,42 @@ class BusStopCard extends GenericCard { /// Returns a widget with the bus stop card final content Widget getCardContent( - BuildContext context, Map stopData, busStopStatus) { + BuildContext context, + Map stopData, + RequestStatus busStopStatus, +) { switch (busStopStatus) { case RequestStatus.successful: if (stopData.isNotEmpty) { - return Column(children: [ - getCardTitle(context), - getBusStopsInfo(context, stopData) - ]); + return Column( + children: [ + getCardTitle(context), + getBusStopsInfo(context, stopData) + ], + ); } else { return Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Configura os teus autocarros', - 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())), - ) - ]), + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Configura os teus autocarros', + 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: @@ -74,64 +89,83 @@ Widget getCardContent( children: [ getCardTitle(context), Container( - padding: const EdgeInsets.all(22.0), - child: const Center(child: CircularProgressIndicator())) + padding: const EdgeInsets.all(22), + child: const Center(child: CircularProgressIndicator()), + ) ], ); case RequestStatus.failed: - default: - return Column(children: [ - getCardTitle(context), - Container( - padding: const EdgeInsets.all(8.0), - child: Text('Não foi possível obter informação', - style: Theme.of(context).textTheme.titleMedium)) - ]); + case RequestStatus.none: + return Column( + children: [ + getCardTitle(context), + Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Não foi possível obter informação', + style: Theme.of(context).textTheme.titleMedium, + ), + ) + ], + ); } } /// Returns a widget for the title of the bus stops card -Widget getCardTitle(context) { +Widget getCardTitle(BuildContext context) { return Row( children: [ const Icon(Icons.directions_bus), // color lightgrey - Text('STCP - Próximas Viagens', - style: Theme.of(context).textTheme.titleMedium), + Text( + 'STCP - Próximas Viagens', + style: Theme.of(context).textTheme.titleMedium, + ), ], ); } /// Returns a widget for all the bus stops info -Widget getBusStopsInfo(context, Map stopData) { +Widget getBusStopsInfo( + BuildContext context, + Map stopData, +) { if (stopData.isNotEmpty) { return Container( - padding: const EdgeInsets.all(4.0), - child: Column( - children: getEachBusStopInfo(context, stopData), - )); + padding: const EdgeInsets.all(4), + child: Column( + children: getEachBusStopInfo(context, stopData), + ), + ); } else { return const Center( - child: Text('Não há dados a mostrar neste momento', - maxLines: 2, overflow: TextOverflow.fade), + child: Text( + 'Não há dados a mostrar neste momento', + maxLines: 2, + overflow: TextOverflow.fade, + ), ); } } /// Returns a list of widgets for each bus stop info that exists -List getEachBusStopInfo(context, Map stopData) { - final List rows = []; - - rows.add(const LastUpdateTimeStamp()); +List getEachBusStopInfo( + BuildContext context, + Map stopData, +) { + final rows = [const LastUpdateTimeStamp()]; stopData.forEach((stopCode, stopInfo) { if (stopInfo.trips.isNotEmpty && stopInfo.favorited) { - rows.add(Container( - padding: const EdgeInsets.only(top: 12.0), + rows.add( + Container( + padding: const EdgeInsets.only(top: 12), child: BusStopRow( stopCode: stopCode, trips: stopInfo.trips, singleTrip: true, - ))); + ), + ), + ); } }); diff --git a/uni/lib/view/home/widgets/exam_card.dart b/uni/lib/view/home/widgets/exam_card.dart index b7b7f4fe9..0ce9e46de 100644 --- a/uni/lib/view/home/widgets/exam_card.dart +++ b/uni/lib/view/home/widgets/exam_card.dart @@ -14,17 +14,19 @@ import 'package:uni/view/lazy_consumer.dart'; /// Manages the exam card section inside the personal area. class ExamCard extends GenericCard { - ExamCard({Key? key}) : super(key: key); + ExamCard({super.key}); const ExamCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override String getTitle() => 'Exames'; @override - onClick(BuildContext context) => + Future onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navExams.title}'); @override @@ -38,27 +40,31 @@ class ExamCard extends GenericCard { /// 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 List exams = filteredExams - .where((exam) => (!hiddenExams.contains(exam.id))) - .toList(); - return RequestDependentWidgetBuilder( - status: examProvider.status, - builder: () => generateExams(exams, context), - hasContentPredicate: exams.isNotEmpty, - onNullContent: Center( - child: Text('Não existem exames para apresentar', - style: Theme.of(context).textTheme.titleLarge), - ), - contentLoadingWidget: const ExamCardShimmer().build(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, + onNullContent: Center( + child: Text( + 'Não existem exames para apresentar', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + contentLoadingWidget: const ExamCardShimmer().build(context), + ); + }, + ); } /// Returns a widget with all the exams. - Widget generateExams(dynamic exams, BuildContext context) { + Widget generateExams(List exams, BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: getExamRows(context, exams), @@ -68,21 +74,27 @@ class ExamCard extends GenericCard { /// 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 List rows = []; - for (int i = 0; i < 1 && i < exams.length; i++) { + 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.0, left: 80.0, top: 15, bottom: 7), - decoration: BoxDecoration( + 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))), - )); + bottom: BorderSide( + width: 1.5, + color: Theme.of(context).dividerColor, + ), + ), + ), + ), + ); } - for (int i = 1; i < 4 && i < exams.length; i++) { + for (var i = 1; i < 4 && i < exams.length; i++) { rows.add(createSecondaryRowFromExam(context, exams[i])); } return rows; @@ -91,17 +103,20 @@ class ExamCard extends GenericCard { /// Creates a row with the closest exam (which appears separated from the /// others in the card). Widget createRowFromExam(BuildContext context, Exam exam) { - return Column(children: [ - DateRectangle( - date: '${exam.weekDay}, ${exam.begin.day} de ${exam.month}'), - RowContainer( - child: ExamRow( - exam: exam, - teacher: '', - mainPage: true, + return Column( + children: [ + DateRectangle( + date: '${exam.weekDay}, ${exam.begin.day} de ${exam.month}', ), - ), - ]); + RowContainer( + child: ExamRow( + exam: exam, + teacher: '', + mainPage: true, + ), + ), + ], + ); } /// Creates a row for the exams which will be displayed under the closest @@ -114,17 +129,19 @@ class ExamCard extends GenericCard { child: Container( padding: const EdgeInsets.all(11), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '${exam.begin.day} de ${exam.month}', - style: Theme.of(context).textTheme.bodyLarge, - ), - ExamTitle( - subject: exam.subject, type: exam.type, reverseOrder: true) - ]), + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${exam.begin.day} de ${exam.month}', + style: Theme.of(context).textTheme.bodyLarge, + ), + ExamTitle( + subject: exam.subject, + type: exam.type, + reverseOrder: true, + ) + ], + ), ), ), ); diff --git a/uni/lib/view/home/widgets/exam_card_shimmer.dart b/uni/lib/view/home/widgets/exam_card_shimmer.dart index 55cb29ee3..083d1ea5c 100644 --- a/uni/lib/view/home/widgets/exam_card_shimmer.dart +++ b/uni/lib/view/home/widgets/exam_card_shimmer.dart @@ -1,103 +1,98 @@ import 'package:flutter/material.dart'; class ExamCardShimmer extends StatelessWidget { - const ExamCardShimmer({Key? key}) : super(key: key); + const ExamCardShimmer({super.key}); @override Widget build(BuildContext context) { return Center( - child: Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ + child: Container( + padding: const EdgeInsets.only(left: 12, bottom: 8, right: 12), + margin: const EdgeInsets.only(top: 8), + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + height: 2.5, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + ], + ) + ], + ), + Container( + height: 30, + width: 100, + color: Colors.black, + ), //UC section + Container( + height: 40, + width: 40, + color: Colors.black, + ), //Calender add section + ], + ), + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Container( - margin: const EdgeInsets.only(top: 8, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ - //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - height: 2.5, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) - ]), - Container( - height: 30, - width: 100, - color: Colors.black, - ), //UC section - Container( - height: 40, - width: 40, - color: Colors.black, - ), //Calender add section - ], - )), + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), const SizedBox( - height: 10, + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + width: 10, + ), + Container( + height: 15, + width: 40, + color: Colors.black, ), - Row( - //Exam room section - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - width: 10, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) ], - ))); + ) + ], + ), + ), + ); } } diff --git a/uni/lib/view/home/widgets/exit_app_dialog.dart b/uni/lib/view/home/widgets/exit_app_dialog.dart index d078f05ac..58be3e236 100644 --- a/uni/lib/view/home/widgets/exit_app_dialog.dart +++ b/uni/lib/view/home/widgets/exit_app_dialog.dart @@ -4,32 +4,35 @@ import 'package:flutter/services.dart'; /// Manages the app section displayed when the user presses the back button class BackButtonExitWrapper extends StatelessWidget { const BackButtonExitWrapper({ - Key? key, required this.context, required this.child, - }) : super(key: key); + super.key, + }); final BuildContext context; final Widget child; - Future backButton() { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Tens a certeza de que pretendes sair?', - style: Theme.of(context).textTheme.headlineSmall), - actions: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Não'), - ), - ElevatedButton( - onPressed: () => SystemChannels.platform - .invokeMethod('SystemNavigator.pop'), - child: const Text('Sim'), - ) - ], - )); + Future backButton() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + 'Tens a certeza de que pretendes sair?', + style: Theme.of(context).textTheme.headlineSmall, + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Não'), + ), + ElevatedButton( + onPressed: () => + SystemChannels.platform.invokeMethod('SystemNavigator.pop'), + child: const Text('Sim'), + ) + ], + ), + ); } @override diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index 72656370f..a6bee59e3 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -16,87 +16,95 @@ import 'package:uni/view/library/widgets/library_occupation_card.dart'; import 'package:uni/view/profile/widgets/account_info_card.dart'; typedef CardCreator = GenericCard Function( - Key key, bool isEditingMode, dynamic Function()? onDelete); + Key key, { + required bool editingMode, + void Function()? onDelete, +}); class MainCardsList extends StatelessWidget { + const MainCardsList({super.key}); static Map cardCreators = { - FavoriteWidgetType.schedule: (k, em, od) => - ScheduleCard.fromEditingInformation(k, em, od), - FavoriteWidgetType.exams: (k, em, od) => - ExamCard.fromEditingInformation(k, em, od), - FavoriteWidgetType.account: (k, em, od) => - AccountInfoCard.fromEditingInformation(k, em, od), + FavoriteWidgetType.schedule: ScheduleCard.fromEditingInformation, + FavoriteWidgetType.exams: ExamCard.fromEditingInformation, + FavoriteWidgetType.account: AccountInfoCard.fromEditingInformation, - // TODO: Bring print card back when it is ready + // TODO(bdmendes): Bring print card back when it is ready /*FavoriteWidgetType.printBalance: (k, em, od) => PrintInfoCard.fromEditingInformation(k, em, od),*/ - FavoriteWidgetType.busStops: (k, em, od) => - BusStopCard.fromEditingInformation(k, em, od), - FavoriteWidgetType.libraryOccupation: (k, em, od) => - LibraryOccupationCard.fromEditingInformation(k, em, od) + FavoriteWidgetType.busStops: BusStopCard.fromEditingInformation, + FavoriteWidgetType.libraryOccupation: + LibraryOccupationCard.fromEditingInformation }; - const MainCardsList({super.key}); - @override Widget build(BuildContext context) { return LazyConsumer( - builder: (context, homePageProvider) => Scaffold( - body: BackButtonExitWrapper( - context: context, - 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, - context, - homePageProvider), - ) - : ListView( - children: [ - createTopBar(context, homePageProvider), - ...favoriteCardsFromTypes( - homePageProvider.favoriteCards, - context, - homePageProvider) - ], - )), - ), - floatingActionButton: homePageProvider.isEditing - ? createActionButton(context) - : null, - )); + builder: (context, homePageProvider) => Scaffold( + body: BackButtonExitWrapper( + context: context, + 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, + context, + homePageProvider, + ), + ) + : ListView( + children: [ + createTopBar(context, homePageProvider), + ...favoriteCardsFromTypes( + homePageProvider.favoriteCards, + context, + homePageProvider, + ) + ], + ), + ), + ), + floatingActionButton: + homePageProvider.isEditing ? createActionButton(context) : null, + ), + ); } Widget createActionButton(BuildContext context) { return FloatingActionButton( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - 'Escolhe um widget para adicionares à tua área pessoal:', - style: Theme.of(context).textTheme.headlineSmall), - content: SizedBox( - height: 200.0, - width: 100.0, - child: ListView(children: getCardAdders(context)), + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Escolhe um widget para adicionares à tua área pessoal:', + style: Theme.of(context).textTheme.headlineSmall, + ), + content: SizedBox( + height: 200, + width: 100, + child: ListView(children: getCardAdders(context)), + ), + actions: [ + TextButton( + child: Text( + 'Cancelar', + style: Theme.of(context).textTheme.bodyMedium, ), - actions: [ - TextButton( - child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), - onPressed: () => Navigator.pop(context)) - ]); - }), //Add FAB functionality here + onPressed: () => Navigator.pop(context), + ) + ], + ); + }, + ), //Add FAB functionality here tooltip: 'Adicionar widget', child: Icon(Icons.add, color: Theme.of(context).colorScheme.onPrimary), ); @@ -105,54 +113,74 @@ class MainCardsList extends StatelessWidget { List getCardAdders(BuildContext context) { final session = Provider.of(context, listen: false).session; - final List favorites = + final favorites = Provider.of(context, listen: false).favoriteCards; final possibleCardAdditions = cardCreators.entries .where((e) => e.key.isVisible(session.faculties)) .where((e) => !favorites.contains(e.key)) - .map((e) => Container( - decoration: const BoxDecoration(), - child: ListTile( - title: Text( - e.value(Key(e.key.index.toString()), false, null).getTitle(), - textAlign: TextAlign.center, - ), - onTap: () { - addCardToFavorites(e.key, context); - Navigator.pop(context); - }, + .map( + (e) => DecoratedBox( + decoration: const BoxDecoration(), + child: ListTile( + title: Text( + e + .value(Key(e.key.index.toString()), editingMode: false) + .getTitle(), + textAlign: TextAlign.center, ), - )) + onTap: () { + addCardToFavorites(e.key, context); + Navigator.pop(context); + }, + ), + ), + ) .toList(); return possibleCardAdditions.isEmpty ? [ const Text( - '''Todos os widgets disponíveis já foram adicionados à tua área pessoal!''') + '''Todos os widgets disponíveis já foram adicionados à tua área pessoal!''', + ) ] : possibleCardAdditions; } Widget createTopBar( - BuildContext context, HomePageProvider editingModeProvider) { + BuildContext context, + HomePageProvider editingModeProvider, + ) { return Container( padding: const EdgeInsets.fromLTRB(20, 20, 20, 5), - child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - PageTitle( - name: DrawerItem.navPersonalArea.title, center: false, pad: false), - GestureDetector( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PageTitle( + name: DrawerItem.navPersonalArea.title, + center: false, + pad: false, + ), + GestureDetector( onTap: () => Provider.of(context, listen: false) - .setHomePageEditingMode(!editingModeProvider.isEditing), + .setHomePageEditingMode( + editingMode: !editingModeProvider.isEditing, + ), child: Text( - editingModeProvider.isEditing ? 'Concluir Edição' : 'Editar', - style: Theme.of(context).textTheme.bodySmall)) - ]), + editingModeProvider.isEditing ? 'Concluir Edição' : 'Editar', + style: Theme.of(context).textTheme.bodySmall, + ), + ) + ], + ), ); } - List favoriteCardsFromTypes(List cardTypes, - BuildContext context, HomePageProvider editingModeProvider) { + List favoriteCardsFromTypes( + List cardTypes, + BuildContext context, + HomePageProvider editingModeProvider, + ) { final userSession = Provider.of(context, listen: false).session; return cardTypes @@ -161,29 +189,35 @@ class MainCardsList extends StatelessWidget { .map((type) { final i = cardTypes.indexOf(type); return cardCreators[type]!( - Key(i.toString()), - editingModeProvider.isEditing, - () => removeCardIndexFromFavorites(i, context)); + Key(i.toString()), + editingMode: editingModeProvider.isEditing, + onDelete: () => removeCardIndexFromFavorites(i, context), + ); }).toList(); } - void reorderCard(int oldIndex, int newIndex, - List favorites, BuildContext context) { - final FavoriteWidgetType tmp = favorites[oldIndex]; - favorites.removeAt(oldIndex); - favorites.insert(oldIndex < newIndex ? newIndex - 1 : newIndex, tmp); + void reorderCard( + int oldIndex, + int newIndex, + List favorites, + BuildContext context, + ) { + final tmp = favorites[oldIndex]; + favorites + ..removeAt(oldIndex) + ..insert(oldIndex < newIndex ? newIndex - 1 : newIndex, tmp); saveFavoriteCards(context, favorites); } void removeCardIndexFromFavorites(int i, BuildContext context) { - final List favorites = - Provider.of(context, listen: false).favoriteCards; - favorites.removeAt(i); + final favorites = Provider.of(context, listen: false) + .favoriteCards + ..removeAt(i); saveFavoriteCards(context, favorites); } void addCardToFavorites(FavoriteWidgetType type, BuildContext context) { - final List favorites = + final favorites = Provider.of(context, listen: false).favoriteCards; if (!favorites.contains(type)) { favorites.add(type); @@ -192,7 +226,9 @@ class MainCardsList extends StatelessWidget { } void saveFavoriteCards( - BuildContext context, List favorites) { + BuildContext context, + List favorites, + ) { Provider.of(context, listen: false) .setFavoriteCards(favorites); AppSharedPreferences.saveFavoriteCards(favorites); diff --git a/uni/lib/view/home/widgets/restaurant_card.dart b/uni/lib/view/home/widgets/restaurant_card.dart index 8efb549e8..a88fb6d1f 100644 --- a/uni/lib/view/home/widgets/restaurant_card.dart +++ b/uni/lib/view/home/widgets/restaurant_card.dart @@ -1,5 +1,8 @@ +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/view/common_widgets/date_rectangle.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; @@ -9,17 +12,19 @@ import 'package:uni/view/home/widgets/restaurant_row.dart'; import 'package:uni/view/lazy_consumer.dart'; class RestaurantCard extends GenericCard { - RestaurantCard({Key? key}) : super(key: key); + RestaurantCard({super.key}); const RestaurantCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override String getTitle() => 'Cantinas'; @override - onClick(BuildContext context) {} + void onClick(BuildContext context) {} @override void onRefresh(BuildContext context) { @@ -30,45 +35,57 @@ class RestaurantCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, restaurantProvider) => RequestDependentWidgetBuilder( - status: restaurantProvider.status, - builder: () => - generateRestaurant(restaurantProvider.restaurants, context), - hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, - onNullContent: Center( - child: Text('Não existem cantinas para apresentar', - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center)))); + builder: (context, restaurantProvider) => RequestDependentWidgetBuilder( + status: restaurantProvider.status, + builder: () => + generateRestaurant(restaurantProvider.restaurants, context), + hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, + onNullContent: Center( + child: Text( + 'Não existem cantinas para apresentar', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + ), + ), + ); } - Widget generateRestaurant(canteens, context) { + Widget generateRestaurant( + UnmodifiableListView canteens, + BuildContext context, + ) { return Column( mainAxisSize: MainAxisSize.min, - children: [createRowFromRestaurant(context, canteens)], + children: [createRowFromRestaurant(context, canteens.toString())], ); } - Widget createRowFromRestaurant(context, String canteen) { - // TODO: Issue #390 - return Column(children: [ - const DateRectangle(date: ''), // TODO: Issue #390 - // cantine.nextSchoolDay - Center( + Widget createRowFromRestaurant(BuildContext context, String canteen) { + return Column( + children: [ + const DateRectangle(date: ''), + // cantine.nextSchoolDay + Center( child: Container( - padding: const EdgeInsets.all(12.0), child: Text(canteen))), - Card( - elevation: 1, - child: RowContainer( + padding: const EdgeInsets.all(12), + child: Text(canteen), + ), + ), + Card( + elevation: 1, + child: RowContainer( color: const Color.fromARGB(0, 0, 0, 0), child: RestaurantRow( local: canteen, meatMenu: '', - // TODO: Issue #390 fishMenu: '', vegetarianMenu: '', dietMenu: '', - )), - ), - ]); + ), + ), + ), + ], + ); } } diff --git a/uni/lib/view/home/widgets/restaurant_row.dart b/uni/lib/view/home/widgets/restaurant_row.dart index 0d7a23589..7a62039e1 100644 --- a/uni/lib/view/home/widgets/restaurant_row.dart +++ b/uni/lib/view/home/widgets/restaurant_row.dart @@ -2,66 +2,71 @@ import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class RestaurantRow extends StatelessWidget { - final String local; - final String meatMenu; - final String fishMenu; - final String vegetarianMenu; - final String dietMenu; - final double iconSize; - const RestaurantRow({ - Key? key, required this.local, required this.meatMenu, required this.fishMenu, required this.vegetarianMenu, required this.dietMenu, + super.key, this.iconSize = 20.0, - }) : super(key: key); + }); + final String local; + final String meatMenu; + final String fishMenu; + final String vegetarianMenu; + final String dietMenu; + final double iconSize; @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(left: 12, bottom: 8, right: 12), + margin: const EdgeInsets.only(top: 8), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: Column( - children: getMenuRows(context), - )) + child: Column( + children: getMenuRows(context), + ), + ) ], ), ); } List getMenuRows(BuildContext context) { - final List widgets = []; - final List meals = [meatMenu, fishMenu, vegetarianMenu, dietMenu]; - final Map mealIcon = { + final widgets = []; + final meals = [meatMenu, fishMenu, vegetarianMenu, dietMenu]; + final mealIcon = { meatMenu: MdiIcons.foodDrumstickOutline, fishMenu: MdiIcons.fish, vegetarianMenu: MdiIcons.corn, dietMenu: MdiIcons.nutrition }; - for (var element in meals) { - widgets.add(Container( - padding: const EdgeInsets.all(12.0), + for (final element in meals) { + widgets.add( + Container( + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.7, - color: Theme.of(context).colorScheme.secondary))), + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(mealIcon[element], size: iconSize), - Expanded(child: Text(element, textAlign: TextAlign.center)), - ]))); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(mealIcon[element], size: iconSize), + Expanded(child: Text(element, textAlign: TextAlign.center)), + ], + ), + ), + ); } return widgets; diff --git a/uni/lib/view/home/widgets/schedule_card.dart b/uni/lib/view/home/widgets/schedule_card.dart index a525b8e96..ac86fa085 100644 --- a/uni/lib/view/home/widgets/schedule_card.dart +++ b/uni/lib/view/home/widgets/schedule_card.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/entities/lecture.dart'; @@ -12,14 +14,16 @@ import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class ScheduleCard extends GenericCard { - ScheduleCard({Key? key}) : super(key: key); + ScheduleCard({super.key}); ScheduleCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); - final double borderRadius = 12.0; - final double leftPadding = 12.0; + final double borderRadius = 12; + final double leftPadding = 12; final List lectures = []; @override @@ -30,18 +34,26 @@ class ScheduleCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, lectureProvider) => RequestDependentWidgetBuilder( - status: lectureProvider.status, - builder: () => generateSchedule(lectureProvider.lectures, context), - hasContentPredicate: lectureProvider.lectures.isNotEmpty, - onNullContent: Center( - child: Text('Não existem aulas para apresentar', - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center)), - contentLoadingWidget: const ScheduleCardShimmer().build(context))); + builder: (context, lectureProvider) => RequestDependentWidgetBuilder( + status: lectureProvider.status, + builder: () => generateSchedule(lectureProvider.lectures, context), + hasContentPredicate: lectureProvider.lectures.isNotEmpty, + onNullContent: Center( + child: Text( + 'Não existem aulas para apresentar', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + contentLoadingWidget: const ScheduleCardShimmer().build(context), + ), + ); } - Widget generateSchedule(lectures, BuildContext context) { + Widget generateSchedule( + UnmodifiableListView lectures, + BuildContext context, + ) { final lectureList = List.of(lectures); return Column( mainAxisSize: MainAxisSize.min, @@ -50,19 +62,22 @@ class ScheduleCard extends GenericCard { } List getScheduleRows(BuildContext context, List lectures) { - final List rows = []; + final rows = []; final now = DateTime.now(); var added = 0; // Lectures added to widget - DateTime lastAddedLectureDate = DateTime.now(); // Day of last added lecture + var lastAddedLectureDate = DateTime.now(); // Day of last added lecture - for (int i = 0; added < 2 && i < lectures.length; i++) { + for (var i = 0; added < 2 && i < lectures.length; i++) { if (now.compareTo(lectures[i].endTime) < 0) { if (lastAddedLectureDate.weekday != lectures[i].startTime.weekday && lastAddedLectureDate.compareTo(lectures[i].startTime) <= 0) { - rows.add(DateRectangle( + rows.add( + DateRectangle( date: TimeString.getWeekdaysStrings()[ - (lectures[i].startTime.weekday - 1) % 7])); + (lectures[i].startTime.weekday - 1) % 7], + ), + ); } rows.add(createRowFromLecture(context, lectures[i])); @@ -72,33 +87,38 @@ class ScheduleCard extends GenericCard { } if (rows.isEmpty) { - rows.add(DateRectangle( - date: TimeString.getWeekdaysStrings()[ - lectures[0].startTime.weekday % 7])); - rows.add(createRowFromLecture(context, lectures[0])); + rows + ..add( + DateRectangle( + date: TimeString.getWeekdaysStrings()[ + lectures[0].startTime.weekday % 7], + ), + ) + ..add(createRowFromLecture(context, lectures[0])); } return rows; } - Widget createRowFromLecture(context, Lecture lecture) { + Widget createRowFromLecture(BuildContext context, Lecture lecture) { return Container( - margin: const EdgeInsets.only(bottom: 10), - child: ScheduleSlot( - subject: lecture.subject, - rooms: lecture.room, - begin: lecture.startTime, - end: lecture.endTime, - teacher: lecture.teacher, - typeClass: lecture.typeClass, - classNumber: lecture.classNumber, - occurrId: lecture.occurrId, - )); + margin: const EdgeInsets.only(bottom: 10), + child: ScheduleSlot( + subject: lecture.subject, + rooms: lecture.room, + begin: lecture.startTime, + end: lecture.endTime, + teacher: lecture.teacher, + typeClass: lecture.typeClass, + classNumber: lecture.classNumber, + occurrId: lecture.occurrId, + ), + ); } @override String getTitle() => 'Horário'; @override - onClick(BuildContext context) => + Future onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navSchedule.title}'); } diff --git a/uni/lib/view/home/widgets/schedule_card_shimmer.dart b/uni/lib/view/home/widgets/schedule_card_shimmer.dart index 506ac0621..4c572d050 100644 --- a/uni/lib/view/home/widgets/schedule_card_shimmer.dart +++ b/uni/lib/view/home/widgets/schedule_card_shimmer.dart @@ -1,50 +1,44 @@ import 'package:flutter/material.dart'; class ScheduleCardShimmer extends StatelessWidget { - const ScheduleCardShimmer({Key? key}) : super(key: key); + const ScheduleCardShimmer({super.key}); Widget _getSingleScheduleWidget(BuildContext context) { return Center( - child: Container( - padding: const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12), - margin: const EdgeInsets.only(top: 8.0), child: Container( + padding: const EdgeInsets.only(left: 12, bottom: 8, right: 12), + margin: const EdgeInsets.only(top: 8), + child: Container( margin: const EdgeInsets.only(top: 8, bottom: 8), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ - //timestamp section - Container( - height: 15, - width: 40, - color: Colors.black, - ), - const SizedBox( - height: 2.5, - ), - Container( - height: 15, - width: 40, - color: Colors.black, - ), - ], - ) - ]), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + //timestamp section + Container( + height: 15, + width: 40, + color: Colors.black, + ), + const SizedBox( + height: 2.5, + ), + Container( + height: 15, + width: 40, + color: Colors.black, + ), + ], + ) + ], + ), Column( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( height: 25, @@ -67,16 +61,16 @@ class ScheduleCardShimmer extends StatelessWidget { color: Colors.black, ), //Room section ], - )), - )); + ), + ), + ), + ); } @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( height: 15, diff --git a/uni/lib/view/lazy_consumer.dart b/uni/lib/view/lazy_consumer.dart index 6a83b135f..d130e70a5 100644 --- a/uni/lib/view/lazy_consumer.dart +++ b/uni/lib/view/lazy_consumer.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; @@ -10,12 +11,11 @@ import 'package:uni/model/providers/state_provider_notifier.dart'; /// If the provider depends on the session, it will ensure that SessionProvider /// and ProfileProvider are initialized before initializing itself. class LazyConsumer extends StatelessWidget { - final Widget Function(BuildContext, T) builder; - const LazyConsumer({ - Key? key, required this.builder, - }) : super(key: key); + super.key, + }); + final Widget Function(BuildContext, T) builder; @override Widget build(BuildContext context) { @@ -42,14 +42,15 @@ class LazyConsumer extends StatelessWidget { if (context.mounted) { await provider.ensureInitializedFromRemote(context); } - } catch (_) { - // The provider won't be initialized - // Should only happen in tests + } catch (e) { + Logger().e('Failed to initialize provider: ', e); } }); - return Consumer(builder: (context, provider, _) { - return builder(context, provider); - }); + return Consumer( + builder: (context, provider, _) { + return builder(context, provider); + }, + ); } } diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index 6503b84a6..bacb1cac2 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -9,7 +9,7 @@ import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; class LibraryPageView extends StatefulWidget { - const LibraryPageView({Key? key}) : super(key: key); + const LibraryPageView({super.key}); @override State createState() => LibraryPageViewState(); @@ -19,8 +19,9 @@ class LibraryPageViewState extends GeneralPageViewState { @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, libraryOccupationProvider) => - LibraryPage(libraryOccupationProvider.occupation)); + builder: (context, libraryOccupationProvider) => + LibraryPage(libraryOccupationProvider.occupation), + ); } @override @@ -31,28 +32,32 @@ class LibraryPageViewState extends GeneralPageViewState { } class LibraryPage extends StatelessWidget { - final LibraryOccupation? occupation; - const LibraryPage(this.occupation, {super.key}); + final LibraryOccupation? occupation; @override Widget build(BuildContext context) { return ListView( - scrollDirection: Axis.vertical, - shrinkWrap: true, - children: [ - const PageTitle(name: 'Biblioteca'), - LibraryOccupationCard(), - if (occupation != null) const PageTitle(name: 'Pisos'), - if (occupation != null) getFloorRows(context, occupation!), - ]); + shrinkWrap: true, + children: [ + const PageTitle(name: 'Biblioteca'), + LibraryOccupationCard(), + if (occupation != null) const PageTitle(name: 'Pisos'), + if (occupation != null) getFloorRows(context, occupation!), + ], + ); } Widget getFloorRows(BuildContext context, LibraryOccupation occupation) { - final List floors = []; - for (int i = 1; i < occupation.floors.length; i += 2) { - floors.add(createFloorRow( - context, occupation.getFloor(i), occupation.getFloor(i + 1))); + final floors = []; + for (var i = 1; i < occupation.floors.length; i += 2) { + floors.add( + createFloorRow( + context, + occupation.getFloor(i), + occupation.getFloor(i + 1), + ), + ); } return Column( children: floors, @@ -60,47 +65,62 @@ class LibraryPage extends StatelessWidget { } Widget createFloorRow( - BuildContext context, FloorOccupation floor1, FloorOccupation floor2) { - return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - createFloorCard(context, floor1), - createFloorCard(context, floor2), - ]); + BuildContext context, + FloorOccupation floor1, + FloorOccupation floor2, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + createFloorCard(context, floor1), + createFloorCard(context, floor2), + ], + ); } Widget createFloorCard(BuildContext context, FloorOccupation floor) { return Container( margin: const EdgeInsets.symmetric(vertical: 10), - height: 150.0, - width: 150.0, - padding: const EdgeInsets.all(20.0), + height: 150, + width: 150, + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - color: Theme.of(context).cardColor, - boxShadow: const [ - BoxShadow( - color: Color.fromARGB(0x1c, 0, 0, 0), - blurRadius: 7.0, - offset: Offset(0.0, 1.0), - ) - ]), - child: - Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text('Piso ${floor.number}', - style: Theme.of(context).textTheme.headlineSmall), - Text('${floor.percentage}%', - style: Theme.of(context).textTheme.titleLarge), - Text('${floor.occupation}/${floor.capacity}', + borderRadius: const BorderRadius.all(Radius.circular(10)), + color: Theme.of(context).cardColor, + boxShadow: const [ + BoxShadow( + color: Color.fromARGB(0x1c, 0, 0, 0), + blurRadius: 7, + offset: Offset(0, 1), + ) + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + 'Piso ${floor.number}', + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + '${floor.percentage}%', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + '${floor.occupation}/${floor.capacity}', style: Theme.of(context) .textTheme .titleLarge - ?.copyWith(color: Theme.of(context).colorScheme.background)), - LinearPercentIndicator( - lineHeight: 7.0, - percent: floor.percentage / 100, - progressColor: Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).dividerColor, - ) - ]), + ?.copyWith(color: Theme.of(context).colorScheme.background), + ), + LinearPercentIndicator( + lineHeight: 7, + percent: floor.percentage / 100, + progressColor: Theme.of(context).colorScheme.secondary, + backgroundColor: Theme.of(context).dividerColor, + ) + ], + ), ); } } diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index d4265d2c5..bd45a6678 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:provider/provider.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'; @@ -10,17 +11,19 @@ import 'package:uni/view/lazy_consumer.dart'; /// Manages the library card section inside the personal area. class LibraryOccupationCard extends GenericCard { - LibraryOccupationCard({Key? key}) : super(key: key); + LibraryOccupationCard({super.key}); const LibraryOccupationCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override String getTitle() => 'Ocupação da Biblioteca'; @override - onClick(BuildContext context) => + Future onClick(BuildContext context) => Navigator.pushNamed(context, '/${DrawerItem.navLibrary.title}'); @override @@ -32,44 +35,59 @@ 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())); + builder: (context, libraryOccupationProvider) => + RequestDependentWidgetBuilder( + status: libraryOccupationProvider.status, + builder: () => generateOccupation( + libraryOccupationProvider.occupation, + context, + ), + hasContentPredicate: + libraryOccupationProvider.status != RequestStatus.busy, + onNullContent: const CircularProgressIndicator(), + ), + ); } - Widget generateOccupation(occupation, context) { + Widget generateOccupation( + LibraryOccupation? occupation, + BuildContext context, + ) { if (occupation == null || occupation.capacity == 0) { return Center( - child: Text('Não existem dados para apresentar', - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center)); + child: Text( + 'Não existem dados para apresentar', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ); } return Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), - child: CircularPercentIndicator( - radius: 60.0, - lineWidth: 8.0, - percent: occupation.percentage / 100, - center: Text('${occupation.percentage}%', - style: Theme.of(context) - .textTheme - .displayMedium - ?.copyWith(fontSize: 23, fontWeight: FontWeight.w500)), - footer: Column( - children: [ - const Padding(padding: EdgeInsets.fromLTRB(0, 5.0, 0, 0)), - Text('${occupation.occupation}/${occupation.capacity}', - style: Theme.of(context).textTheme.headlineSmall), - ], - ), - circularStrokeCap: CircularStrokeCap.square, - backgroundColor: Theme.of(context).dividerColor, - progressColor: Theme.of(context).colorScheme.secondary, - )); + padding: const EdgeInsets.symmetric(vertical: 6), + child: CircularPercentIndicator( + radius: 60, + lineWidth: 8, + percent: occupation.percentage / 100, + center: Text( + '${occupation.percentage}%', + style: Theme.of(context) + .textTheme + .displayMedium + ?.copyWith(fontSize: 23, fontWeight: FontWeight.w500), + ), + footer: Column( + children: [ + const Padding(padding: EdgeInsets.fromLTRB(0, 5, 0, 0)), + Text( + '${occupation.occupation}/${occupation.capacity}', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + circularStrokeCap: CircularStrokeCap.square, + backgroundColor: Theme.of(context).dividerColor, + progressColor: Theme.of(context).colorScheme.secondary, + ), + ); } } diff --git a/uni/lib/view/locations/locations.dart b/uni/lib/view/locations/locations.dart index e2905c0c0..777647ac2 100644 --- a/uni/lib/view/locations/locations.dart +++ b/uni/lib/view/locations/locations.dart @@ -9,7 +9,7 @@ import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationsPage extends StatefulWidget { - const LocationsPage({Key? key}) : super(key: key); + const LocationsPage({super.key}); @override LocationsPageState createState() => LocationsPageState(); @@ -29,8 +29,9 @@ class LocationsPageState extends GeneralPageViewState return LazyConsumer( builder: (context, locationsProvider) { return LocationsPageView( - locations: locationsProvider.locations, - status: locationsProvider.status); + locations: locationsProvider.locations, + status: locationsProvider.status, + ); }, ); } @@ -40,33 +41,38 @@ class LocationsPageState extends GeneralPageViewState } class LocationsPageView extends StatelessWidget { + const LocationsPageView({ + required this.locations, + required this.status, + super.key, + }); final List locations; final RequestStatus status; - const LocationsPageView( - {super.key, required this.locations, required this.status}); - @override Widget build(BuildContext context) { - return Column(mainAxisSize: MainAxisSize.max, children: [ - Container( + return Column( + children: [ + Container( width: MediaQuery.of(context).size.width * 0.95, - padding: const EdgeInsets.fromLTRB(0, 0, 0, 4.0), - child: PageTitle(name: 'Locais: ${getLocation()}')), - Container( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 4), + child: PageTitle(name: 'Locais: ${getLocation()}'), + ), + Container( padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), height: MediaQuery.of(context).size.height * 0.75, alignment: Alignment.center, child: RequestDependentWidgetBuilder( status: status, - builder: () => FacultyMap(faculty: "FEUP", locations: locations), + builder: () => FacultyMap(faculty: 'FEUP', locations: locations), hasContentPredicate: locations.isNotEmpty, onNullContent: const Center(child: Text('Não existem locais disponíveis')), - ) - // TODO: add support for multiple faculties - ) - ]); + ), + // TODO(bdmendes): add support for multiple faculties + ) + ], + ); } String getLocation() { diff --git a/uni/lib/view/locations/widgets/faculty_map.dart b/uni/lib/view/locations/widgets/faculty_map.dart index b0398de0b..6b3203302 100644 --- a/uni/lib/view/locations/widgets/faculty_map.dart +++ b/uni/lib/view/locations/widgets/faculty_map.dart @@ -4,12 +4,10 @@ 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}); final String faculty; final List locations; - const FacultyMap({Key? key, required this.faculty, required this.locations}) - : super(key: key); - @override Widget build(BuildContext context) { switch (faculty) { diff --git a/uni/lib/view/locations/widgets/floorless_marker_popup.dart b/uni/lib/view/locations/widgets/floorless_marker_popup.dart index cc156f16a..9c5548033 100644 --- a/uni/lib/view/locations/widgets/floorless_marker_popup.dart +++ b/uni/lib/view/locations/widgets/floorless_marker_popup.dart @@ -4,63 +4,71 @@ import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/faculty_map.dart'; class FloorlessLocationMarkerPopup extends StatelessWidget { - const FloorlessLocationMarkerPopup(this.locationGroup, - {this.showId = false, super.key}); + const FloorlessLocationMarkerPopup( + this.locationGroup, { + this.showId = false, + super.key, + }); final LocationGroup locationGroup; final bool showId; @override Widget build(BuildContext context) { - final List locations = - locationGroup.floors.values.expand((x) => x).toList(); + final locations = locationGroup.floors.values.expand((x) => x).toList(); return Card( color: Theme.of(context).colorScheme.background.withOpacity(0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( - padding: const EdgeInsets.all(12.0), - child: Wrap( - direction: Axis.vertical, - spacing: 8, - children: (showId - ? [Text(locationGroup.id.toString())] - : []) + - locations - .map((location) => LocationRow(location: location)) - .toList(), - )), + padding: const EdgeInsets.all(12), + child: Wrap( + direction: Axis.vertical, + spacing: 8, + children: (showId + ? [Text(locationGroup.id.toString())] + : []) + + locations + .map((location) => LocationRow(location: location)) + .toList(), + ), + ), ); } List buildLocations(BuildContext context, List locations) { return locations - .map((location) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(location.description(), - textAlign: TextAlign.left, - style: TextStyle(color: FacultyMap.getFontColor(context))) - ], - )) + .map( + (location) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + location.description(), + textAlign: TextAlign.left, + style: TextStyle(color: FacultyMap.getFontColor(context)), + ) + ], + ), + ) .toList(); } } class LocationRow extends StatelessWidget { + const LocationRow({required this.location, super.key}); final Location location; - const LocationRow({Key? key, required this.location}) : super(key: key); - @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ - Text(location.description(), - textAlign: TextAlign.left, - style: TextStyle(color: FacultyMap.getFontColor(context))) + Text( + location.description(), + textAlign: TextAlign.left, + style: TextStyle(color: FacultyMap.getFontColor(context)), + ) ], ); } diff --git a/uni/lib/view/locations/widgets/icons.dart b/uni/lib/view/locations/widgets/icons.dart index 7e3d41972..eea463670 100644 --- a/uni/lib/view/locations/widgets/icons.dart +++ b/uni/lib/view/locations/widgets/icons.dart @@ -1,3 +1,5 @@ +import 'package:flutter/widgets.dart'; + /// Flutter icons LocationIcons /// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com /// This font was generated by FlutterIcon.com, which is derived from Fontello. @@ -13,22 +15,16 @@ /// /// /// -import 'package:flutter/widgets.dart'; - class LocationIcons { LocationIcons._(); static const _kFontFam = 'LocationIcons'; - static const String? _kFontPkg = null; static const IconData bookOpenBlankVariant = - IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe800, fontFamily: _kFontFam); static const IconData bottleSodaClassic = - IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData cashMultiple = - IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData coffee = - IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData printer = - IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe801, fontFamily: _kFontFam); + static const IconData cashMultiple = IconData(0xe802, fontFamily: _kFontFam); + static const IconData coffee = IconData(0xe803, fontFamily: _kFontFam); + static const IconData printer = IconData(0xe804, fontFamily: _kFontFam); } diff --git a/uni/lib/view/locations/widgets/map.dart b/uni/lib/view/locations/widgets/map.dart index 74df51ed3..f810d1a3a 100644 --- a/uni/lib/view/locations/widgets/map.dart +++ b/uni/lib/view/locations/widgets/map.dart @@ -1,90 +1,90 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; import 'package:latlong2/latlong.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:url_launcher/url_launcher.dart'; - import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/floorless_marker_popup.dart'; import 'package:uni/view/locations/widgets/marker.dart'; import 'package:uni/view/locations/widgets/marker_popup.dart'; +import 'package:url_launcher/url_launcher.dart'; class LocationsMap extends StatelessWidget { + LocationsMap({ + required this.northEastBoundary, + required this.southWestBoundary, + required this.center, + required this.locations, + super.key, + }); final PopupController _popupLayerController = PopupController(); final List locations; final LatLng northEastBoundary; final LatLng southWestBoundary; final LatLng center; - LocationsMap( - {Key? key, - required this.northEastBoundary, - required this.southWestBoundary, - required this.center, - required this.locations}) - : super(key: key); - @override Widget build(BuildContext context) { return FlutterMap( - options: MapOptions( - minZoom: 17, - maxZoom: 18, - nePanBoundary: northEastBoundary, - swPanBoundary: southWestBoundary, - center: center, - zoom: 17.5, - rotation: 0, - interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, - onTap: (tapPosition, latlng) => _popupLayerController.hideAllPopups(), - ), - nonRotatedChildren: [ - Align( - alignment: Alignment.bottomRight, - child: ColoredBox( - color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.8), - child: GestureDetector( - onTap: () => launchUrl( - Uri(host: 'openstreetmap.org', path: '/copyright')), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 5, horizontal: 8), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Text("© OpenStreetMap"), - ), + options: MapOptions( + minZoom: 17, + maxZoom: 18, + nePanBoundary: northEastBoundary, + swPanBoundary: southWestBoundary, + center: center, + zoom: 17.5, + interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, + onTap: (tapPosition, latlng) => _popupLayerController.hideAllPopups(), + ), + nonRotatedChildren: [ + Align( + alignment: Alignment.bottomRight, + child: ColoredBox( + color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.8), + child: GestureDetector( + onTap: () => launchUrl( + Uri(host: 'openstreetmap.org', path: '/copyright'), + ), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 8), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Text('© OpenStreetMap'), ), ), ), - ) - ], - children: [ - TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: const ['a', 'b', 'c'], - tileProvider: CachedTileProvider(), ), - PopupMarkerLayer( - options: PopupMarkerLayerOptions( - markers: locations.map((location) { - return LocationMarker(location.latlng, location); - }).toList(), - popupController: _popupLayerController, - popupDisplayOptions: PopupDisplayOptions( - animation: const PopupAnimation.fade( - duration: Duration(milliseconds: 400)), - builder: (_, Marker marker) { - if (marker is LocationMarker) { - return marker.locationGroup.isFloorless - ? FloorlessLocationMarkerPopup(marker.locationGroup) - : LocationMarkerPopup(marker.locationGroup); - } - return const Card(child: Text('undefined')); - }, + ) + ], + children: [ + TileLayer( + urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: const ['a', 'b', 'c'], + tileProvider: CachedTileProvider(), + ), + PopupMarkerLayer( + options: PopupMarkerLayerOptions( + markers: locations.map((location) { + return LocationMarker(location.latlng, location); + }).toList(), + popupController: _popupLayerController, + popupDisplayOptions: PopupDisplayOptions( + animation: const PopupAnimation.fade( + duration: Duration(milliseconds: 400), ), + builder: (_, Marker marker) { + if (marker is LocationMarker) { + return marker.locationGroup.isFloorless + ? FloorlessLocationMarkerPopup(marker.locationGroup) + : LocationMarkerPopup(marker.locationGroup); + } + return const Card(child: Text('undefined')); + }, ), ), - ]); + ), + ], + ); } } diff --git a/uni/lib/view/locations/widgets/marker.dart b/uni/lib/view/locations/widgets/marker.dart index 626f57dff..c7ca2fb5e 100644 --- a/uni/lib/view/locations/widgets/marker.dart +++ b/uni/lib/view/locations/widgets/marker.dart @@ -6,42 +6,42 @@ import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationMarker extends Marker { - final LocationGroup locationGroup; - final LatLng latlng; - LocationMarker(this.latlng, this.locationGroup) : super( anchorPos: AnchorPos.align(AnchorAlign.center), height: 20, width: 20, point: latlng, - builder: (BuildContext ctx) => Container( + builder: (BuildContext ctx) => DecoratedBox( decoration: BoxDecoration( - color: Theme.of(ctx).colorScheme.background, - border: Border.all( - color: Theme.of(ctx).colorScheme.primary, - ), - borderRadius: const BorderRadius.all(Radius.circular(20))), - child: - MarkerIcon(location: locationGroup.getLocationWithMostWeight()), + color: Theme.of(ctx).colorScheme.background, + border: Border.all( + color: Theme.of(ctx).colorScheme.primary, + ), + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: MarkerIcon( + location: locationGroup.getLocationWithMostWeight(), + ), ), ); + final LocationGroup locationGroup; + final LatLng latlng; } class MarkerIcon extends StatelessWidget { + const MarkerIcon({super.key, this.location}); final Location? location; - const MarkerIcon({Key? key, this.location}) : super(key: key); - @override Widget build(BuildContext context) { if (location == null) { return Container(); } - final Color fontColor = FacultyMap.getFontColor(context); + final fontColor = FacultyMap.getFontColor(context); if (location?.icon is IconData) { - return Icon(location?.icon, color: fontColor, size: 12); + return Icon(location?.icon as IconData, color: fontColor, size: 12); } else { return Icon(Icons.device_unknown, color: fontColor, size: 12); } diff --git a/uni/lib/view/locations/widgets/marker_popup.dart b/uni/lib/view/locations/widgets/marker_popup.dart index 7802b1f97..a683f613b 100644 --- a/uni/lib/view/locations/widgets/marker_popup.dart +++ b/uni/lib/view/locations/widgets/marker_popup.dart @@ -4,8 +4,11 @@ import 'package:uni/model/entities/location_group.dart'; import 'package:uni/view/locations/widgets/faculty_map.dart'; class LocationMarkerPopup extends StatelessWidget { - const LocationMarkerPopup(this.locationGroup, - {this.showId = false, super.key}); + const LocationMarkerPopup( + this.locationGroup, { + this.showId = false, + super.key, + }); final LocationGroup locationGroup; final bool showId; @@ -18,84 +21,84 @@ class LocationMarkerPopup extends StatelessWidget { borderRadius: BorderRadius.circular(15), ), child: Padding( - padding: const EdgeInsets.all(12.0), - child: Wrap( - direction: Axis.vertical, - spacing: 8, - children: (showId - ? [Text(locationGroup.id.toString())] - : []) + - getEntries() - .map((entry) => - Floor(floor: entry.key, locations: entry.value)) - .toList(), - )), + padding: const EdgeInsets.all(12), + child: Wrap( + direction: Axis.vertical, + spacing: 8, + children: (showId + ? [Text(locationGroup.id.toString())] + : []) + + getEntries() + .map( + (entry) => Floor(floor: entry.key, locations: entry.value), + ) + .toList(), + ), + ), ); } List>> getEntries() { - final List>> entries = - locationGroup.floors.entries.toList(); - entries.sort((current, next) => -current.key.compareTo(next.key)); - return entries; + return locationGroup.floors.entries.toList() + ..sort((current, next) => -current.key.compareTo(next.key)); } } class Floor extends StatelessWidget { + const Floor({required this.locations, required this.floor, super.key}); final List locations; final int floor; - const Floor({Key? key, required this.locations, required this.floor}) - : super(key: key); - @override Widget build(BuildContext context) { - final Color fontColor = FacultyMap.getFontColor(context); + final fontColor = FacultyMap.getFontColor(context); - final String floorString = - 0 <= floor && floor <= 9 //To maintain layout of popup - ? ' $floor' - : '$floor'; + final floorString = 0 <= floor && floor <= 9 //To maintain layout of popup + ? ' $floor' + : '$floor'; final Widget floorCol = Column( mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: - Text('Andar $floorString', style: TextStyle(color: fontColor))) + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Text('Andar $floorString', style: TextStyle(color: fontColor)), + ) ], ); final Widget locationsColumn = Container( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - decoration: - BoxDecoration(border: Border(left: BorderSide(color: fontColor))), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: locations - .map((location) => - LocationRow(location: location, color: fontColor)) - .toList(), - )); + 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]); } } class LocationRow extends StatelessWidget { + const LocationRow({required this.location, required this.color, super.key}); final Location location; final Color color; - const LocationRow({Key? key, required this.location, required this.color}) - : super(key: key); - @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ - Text(location.description(), - textAlign: TextAlign.left, style: TextStyle(color: color)) + Text( + location.description(), + textAlign: TextAlign.left, + style: TextStyle(color: color), + ) ], ); } diff --git a/uni/lib/view/login/login.dart b/uni/lib/view/login/login.dart index 000ce1926..06d56819b 100644 --- a/uni/lib/view/login/login.dart +++ b/uni/lib/view/login/login.dart @@ -45,7 +45,7 @@ class LoginPageViewState extends State { bool _keepSignedIn = true; bool _obscurePasswordInput = true; - void _login(BuildContext context) async { + Future _login(BuildContext context) async { final stateProviders = StateProviders.fromContext(context); final sessionProvider = stateProviders.sessionProvider; if (sessionProvider.status != RequestStatus.busy && @@ -55,7 +55,11 @@ class LoginPageViewState extends State { try { await sessionProvider.postAuthentication( - user, pass, faculties, _keepSignedIn); + user, + pass, + faculties, + persistentSession: _keepSignedIn, + ); if (context.mounted) { handleLogin(sessionProvider.status, context); } @@ -63,11 +67,11 @@ class LoginPageViewState extends State { if (error is ExpiredCredentialsException) { updatePasswordDialog(); } else if (error is InternetStatusException) { - ToastMessage.warning(context, error.message); + unawaited(ToastMessage.warning(context, error.message)); } else if (error is WrongCredentialsException) { - ToastMessage.error(context, error.message); + unawaited(ToastMessage.error(context, error.message)); } else { - ToastMessage.error(context, 'Erro no login'); + unawaited(ToastMessage.error(context, 'Erro no login')); } } } @@ -75,7 +79,7 @@ class LoginPageViewState extends State { /// Updates the list of faculties /// based on the options the user selected (used as a callback) - void setFaculties(faculties) { + void setFaculties(List faculties) { setState(() { this.faculties = faculties; }); @@ -83,7 +87,8 @@ class LoginPageViewState extends State { /// Tracks if the user wants to keep signed in (has a /// checkmark on the button). - void _setKeepSignedIn(value) { + void _setKeepSignedIn(bool? value) { + if (value == null) return; setState(() { _keepSignedIn = value; }); @@ -98,61 +103,78 @@ class LoginPageViewState extends State { @override Widget build(BuildContext context) { - final MediaQueryData queryData = MediaQuery.of(context); + final queryData = MediaQuery.of(context); return Theme( - data: applicationLightTheme.copyWith( - // The handle color is not applying due to a Flutter bug: - // https://github.com/flutter/flutter/issues/74890 - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Colors.white, selectionHandleColor: Colors.white), - checkboxTheme: CheckboxThemeData( - checkColor: MaterialStateProperty.all(darkRed), - fillColor: MaterialStateProperty.all(Colors.white)), + data: applicationLightTheme.copyWith( + // The handle color is not applying due to a Flutter bug: + // https://github.com/flutter/flutter/issues/74890 + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Colors.white, + selectionHandleColor: Colors.white, ), - child: Builder( - builder: (themeContext) => Scaffold( - backgroundColor: darkRed, - body: WillPopScope( - child: Padding( - padding: EdgeInsets.only( - left: queryData.size.width / 8, - right: queryData.size.width / 8), - child: ListView( - children: [ - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 20)), - createTitle(queryData, context), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 35)), - getLoginForm(queryData, context), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 35)), - createForgetPasswordLink(context), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 15)), - createLogInButton(queryData, context, _login), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 35)), - createStatusWidget(context), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 35)), - createSafeLoginButton(context), - ], - )), - onWillPop: () => onWillPop(themeContext))))); + ), + child: Builder( + builder: (themeContext) => Scaffold( + backgroundColor: darkRed, + body: WillPopScope( + child: Padding( + padding: EdgeInsets.only( + left: queryData.size.width / 8, + right: queryData.size.width / 8, + ), + child: ListView( + children: [ + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 20, + ), + ), + createTitle(queryData, context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35, + ), + ), + getLoginForm(queryData, context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35, + ), + ), + createForgetPasswordLink(context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 15, + ), + ), + createLogInButton(queryData, context, _login), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35, + ), + ), + createStatusWidget(context), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 35, + ), + ), + createSafeLoginButton(context), + ], + ), + ), + onWillPop: () => onWillPop(themeContext), + ), + ), + ), + ); } /// Delay time before the user leaves the app Future exitAppWaiter() async { _exitApp = true; - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); _exitApp = false; } @@ -168,21 +190,25 @@ class LoginPageViewState extends State { } /// Creates the title for the login menu. - Widget createTitle(queryData, context) { + Widget createTitle(MediaQueryData queryData, BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints( - minWidth: queryData.size.width / 8, - minHeight: queryData.size.height / 6, - ), - child: Column(children: [ + constraints: BoxConstraints( + minWidth: queryData.size.width / 8, + minHeight: queryData.size.height / 6, + ), + child: Column( + children: [ SizedBox( - width: 100.0, - child: SvgPicture.asset( - 'assets/images/logo_dark.svg', - colorFilter: - const ColorFilter.mode(Colors.white, BlendMode.srcIn), - )), - ])); + width: 100, + child: SvgPicture.asset( + 'assets/images/logo_dark.svg', + colorFilter: + const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + ], + ), + ); } /// Creates the widgets for the user input fields. @@ -190,22 +216,38 @@ class LoginPageViewState extends State { return Form( key: _formKey, child: SingleChildScrollView( - child: Column(children: [ - createFacultyInput(context, faculties, setFaculties), - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35)), - createUsernameInput( - context, usernameController, usernameFocus, passwordFocus), - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35)), - createPasswordInput( + child: Column( + children: [ + createFacultyInput(context, faculties, setFaculties), + Padding( + padding: EdgeInsets.only(bottom: queryData.size.height / 35), + ), + createUsernameInput( + context, + usernameController, + usernameFocus, + passwordFocus, + ), + Padding( + padding: EdgeInsets.only(bottom: queryData.size.height / 35), + ), + createPasswordInput( context, passwordController, passwordFocus, - _obscurePasswordInput, _toggleObscurePasswordInput, - () => _login(context)), - Padding(padding: EdgeInsets.only(bottom: queryData.size.height / 35)), - createSaveDataCheckBox(_keepSignedIn, _setKeepSignedIn), - ]), + () => _login(context), + obscurePasswordInput: _obscurePasswordInput, + ), + Padding( + padding: EdgeInsets.only(bottom: queryData.size.height / 35), + ), + createSaveDataCheckBox( + _setKeepSignedIn, + keepSignedIn: _keepSignedIn, + ), + ], + ), ), ); } @@ -213,28 +255,31 @@ class LoginPageViewState extends State { ///Creates the widget for when the user forgets the password Widget createForgetPasswordLink(BuildContext context) { return InkWell( - child: Center( - child: Text("Esqueceu a palavra-passe?", - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - decoration: TextDecoration.underline, - color: Colors.white))), - onTap: () => launchUrl(Uri.parse("https://self-id.up.pt/reset"))); + child: Center( + child: Text( + 'Esqueceu a palavra-passe?', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + decoration: TextDecoration.underline, + color: Colors.white, + ), + ), + ), + onTap: () => launchUrl(Uri.parse('https://self-id.up.pt/reset')), + ); } /// Creates a widget for the user login depending on the status of his login. Widget createStatusWidget(BuildContext context) { return Consumer( builder: (context, sessionProvider, _) { - switch (sessionProvider.status) { - case RequestStatus.busy: - return const SizedBox( - height: 60.0, - child: - Center(child: CircularProgressIndicator(color: Colors.white)), - ); - default: - return Container(); + if (sessionProvider.status == RequestStatus.busy) { + return const SizedBox( + height: 60, + child: + Center(child: CircularProgressIndicator(color: Colors.white)), + ); } + return Container(); }, ); } @@ -242,43 +287,48 @@ class LoginPageViewState extends State { void handleLogin(RequestStatus? status, BuildContext context) { if (status == RequestStatus.successful) { Navigator.pushReplacementNamed( - context, '/${DrawerItem.navPersonalArea.title}'); + context, + '/${DrawerItem.navPersonalArea.title}', + ); } } void updatePasswordDialog() { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text("A tua palavra-passe expirou"), + title: const Text('A tua palavra-passe expirou'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( - 'Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.', - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.titleSmall), + 'Por razões de segurança, as palavras-passe têm de ser ' + 'alteradas periodicamente.', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleSmall, + ), const SizedBox(height: 20), const Align( - alignment: Alignment.centerLeft, - child: Text( - 'Deseja alterar a palavra-passe?', - textAlign: TextAlign.start, - )), + alignment: Alignment.centerLeft, + child: Text( + 'Deseja alterar a palavra-passe?', + textAlign: TextAlign.start, + ), + ), ], ), actions: [ TextButton( - child: const Text("Cancelar"), + child: const Text('Cancelar'), onPressed: () { Navigator.of(context).pop(); }, ), ElevatedButton( - child: const Text("Alterar"), + child: const Text('Alterar'), onPressed: () async { - const url = "https://self-id.up.pt/password"; + const url = 'https://self-id.up.pt/password'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } diff --git a/uni/lib/view/login/widgets/faculties_multiselect.dart b/uni/lib/view/login/widgets/faculties_multiselect.dart index ce315d539..91e47c030 100644 --- a/uni/lib/view/login/widgets/faculties_multiselect.dart +++ b/uni/lib/view/login/widgets/faculties_multiselect.dart @@ -2,41 +2,53 @@ import 'package:flutter/material.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 Function setFaculties; - - const FacultiesMultiselect(this.selectedFaculties, this.setFaculties, - {super.key}); + final void Function(List) setFaculties; @override Widget build(BuildContext context) { - const Color textColor = Color.fromARGB(255, 0xfa, 0xfa, 0xfa); + 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)); + 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, - width: 1, - ))), - child: Row(children: [ + 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(), @@ -47,17 +59,19 @@ class FacultiesMultiselect extends StatelessWidget { Icons.arrow_drop_down, color: Colors.white, ), - ])); + ], + ), + ); } String _facultiesListText() { if (selectedFaculties.isEmpty) { return 'sem faculdade'; } - String facultiesText = ''; - for (String faculty in selectedFaculties) { - facultiesText += '${faculty.toUpperCase()}, '; + final buffer = StringBuffer(); + for (final faculty in selectedFaculties) { + buffer.write('${faculty.toUpperCase()}, '); } - return facultiesText.substring(0, facultiesText.length - 2); + 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 index 7ce700eb8..537bf5edd 100644 --- a/uni/lib/view/login/widgets/faculties_selection_form.dart +++ b/uni/lib/view/login/widgets/faculties_selection_form.dart @@ -3,11 +3,13 @@ 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 Function setFaculties; - - const FacultiesSelectionForm(this.selectedFaculties, this.setFaculties, - {super.key}); + final void Function(List) setFaculties; @override State createState() => _FacultiesSelectionFormState(); @@ -17,44 +19,57 @@ class _FacultiesSelectionFormState extends State { @override Widget build(BuildContext context) { return AlertDialog( - backgroundColor: const Color.fromARGB(255, 0x75, 0x17, 0x1e), - title: const Text('seleciona a(s) tua(s) faculdade(s)'), - titleTextStyle: const TextStyle( - color: Color.fromARGB(255, 0xfa, 0xfa, 0xfa), fontSize: 18), - content: SizedBox( - height: 500.0, width: 200.0, child: createCheckList(context)), - actions: createActionButtons(context)); + backgroundColor: const Color.fromARGB(255, 0x75, 0x17, 0x1e), + title: const Text('seleciona a(s) tua(s) faculdade(s)'), + 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: const Text('Cancelar', style: TextStyle(color: Colors.white))), + onPressed: () => Navigator.pop(context), + child: const Text('Cancelar', style: TextStyle(color: Colors.white)), + ), ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, - backgroundColor: Colors.white), - onPressed: () { - if (widget.selectedFaculties.isEmpty) { - ToastMessage.warning( - context, 'Seleciona pelo menos uma faculdade'); - return; - } - Navigator.pop(context); - widget.setFaculties(widget.selectedFaculties); - }, - child: const Text('Confirmar')) + style: ElevatedButton.styleFrom( + foregroundColor: Theme.of(context).primaryColor, + backgroundColor: Colors.white, + ), + onPressed: () { + if (widget.selectedFaculties.isEmpty) { + ToastMessage.warning( + context, + 'Seleciona pelo menos uma faculdade', + ); + return; + } + Navigator.pop(context); + widget.setFaculties(widget.selectedFaculties); + }, + child: const Text('Confirmar'), + ) ]; } Widget createCheckList(BuildContext context) { return ListView( - children: List.generate(constants.faculties.length, (i) { - final String faculty = constants.faculties.elementAt(i); - return CheckboxListTile( - title: Text(faculty.toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 20.0)), + 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) { @@ -65,7 +80,9 @@ class _FacultiesSelectionFormState extends State { widget.selectedFaculties.remove(faculty); } }); - }); - })); + }, + ); + }), + ); } } diff --git a/uni/lib/view/login/widgets/inputs.dart b/uni/lib/view/login/widgets/inputs.dart index b40da8720..d42b805b2 100644 --- a/uni/lib/view/login/widgets/inputs.dart +++ b/uni/lib/view/login/widgets/inputs.dart @@ -1,24 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:uni/view/login/widgets/faculties_multiselect.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, setFaculties) { + BuildContext context, + List faculties, + void Function(List) setFaculties, +) { return FacultiesMultiselect(faculties, setFaculties); } /// Creates the widget for the username input. Widget createUsernameInput( - BuildContext context, - TextEditingController usernameController, - FocusNode usernameFocus, - FocusNode passwordFocus) { + BuildContext context, + TextEditingController usernameController, + FocusNode usernameFocus, + FocusNode passwordFocus, +) { return TextFormField( style: const TextStyle(color: Colors.white, fontSize: 20), enableSuggestions: false, autocorrect: false, - autofocus: false, controller: usernameController, focusNode: usernameFocus, onFieldSubmitted: (term) { @@ -33,34 +36,41 @@ Widget createUsernameInput( } Widget createPasswordInput( - BuildContext context, - TextEditingController passwordController, - FocusNode passwordFocus, - bool obscurePasswordInput, - Function toggleObscurePasswordInput, - Function login) { + BuildContext context, + TextEditingController passwordController, + FocusNode passwordFocus, + void Function() toggleObscurePasswordInput, + void Function() login, { + required bool obscurePasswordInput, +}) { return TextFormField( - style: const TextStyle(color: Colors.white, fontSize: 20), - enableSuggestions: false, - autocorrect: false, - autofocus: false, - controller: passwordController, - focusNode: passwordFocus, - onFieldSubmitted: (term) { - passwordFocus.unfocus(); - login(); - }, - textInputAction: TextInputAction.done, - obscureText: obscurePasswordInput, - textAlign: TextAlign.left, - decoration: passwordFieldDecoration( - 'palavra-passe', obscurePasswordInput, toggleObscurePasswordInput), - validator: (String? value) => - value != null && value.isEmpty ? 'Preenche este campo' : null); + style: const TextStyle(color: Colors.white, fontSize: 20), + enableSuggestions: false, + autocorrect: false, + controller: passwordController, + focusNode: passwordFocus, + onFieldSubmitted: (term) { + passwordFocus.unfocus(); + login(); + }, + textInputAction: TextInputAction.done, + obscureText: obscurePasswordInput, + textAlign: TextAlign.left, + decoration: passwordFieldDecoration( + 'palavra-passe', + toggleObscurePasswordInput, + obscurePasswordInput: obscurePasswordInput, + ), + validator: (String? value) => + value != null && value.isEmpty ? 'Preenche este campo' : null, + ); } /// Creates the widget for the user to keep signed in (save his data). -Widget createSaveDataCheckBox(bool keepSignedIn, setKeepSignedIn) { +Widget createSaveDataCheckBox( + void Function(bool?)? setKeepSignedIn, { + required bool keepSignedIn, +}) { return CheckboxListTile( value: keepSignedIn, onChanged: setKeepSignedIn, @@ -68,16 +78,25 @@ Widget createSaveDataCheckBox(bool keepSignedIn, setKeepSignedIn) { 'Manter sessão iniciada', textAlign: TextAlign.center, style: TextStyle( - color: Colors.white, fontSize: 17.0, fontWeight: FontWeight.w300), + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w300, + ), ), ); } /// Creates the widget for the user to confirm the inputted login info -Widget createLogInButton(queryData, BuildContext context, login) { +Widget createLogInButton( + MediaQueryData queryData, + BuildContext context, + void Function(BuildContext) login, +) { return Padding( padding: EdgeInsets.only( - left: queryData.size.width / 7, right: queryData.size.width / 7), + left: queryData.size.width / 7, + right: queryData.size.width / 7, + ), child: SizedBox( height: queryData.size.height / 16, child: ElevatedButton( @@ -93,12 +112,15 @@ Widget createLogInButton(queryData, BuildContext context, login) { } login(context); }, - child: Text('Entrar', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w400, - fontSize: 20), - textAlign: TextAlign.center), + child: Text( + 'Entrar', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w400, + fontSize: 20, + ), + textAlign: TextAlign.center, + ), ), ), ); @@ -107,73 +129,83 @@ Widget createLogInButton(queryData, BuildContext context, login) { /// Decoration for the username field. InputDecoration textFieldDecoration(String placeholder) { return InputDecoration( - hintStyle: const TextStyle(color: Colors.white), - errorStyle: const TextStyle( - color: Colors.white70, - ), - hintText: placeholder, - contentPadding: const EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0), - border: const UnderlineInputBorder(), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white, width: 3))); + hintStyle: const TextStyle(color: Colors.white), + errorStyle: const TextStyle( + color: Colors.white70, + ), + hintText: placeholder, + contentPadding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + border: const UnderlineInputBorder(), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white, width: 3), + ), + ); } /// Decoration for the password field. InputDecoration passwordFieldDecoration( - String placeholder, bool obscurePasswordInput, toggleObscurePasswordInput) { + String placeholder, + void Function() toggleObscurePasswordInput, { + required bool obscurePasswordInput, +}) { final genericDecoration = textFieldDecoration(placeholder); return InputDecoration( - hintStyle: genericDecoration.hintStyle, - errorStyle: genericDecoration.errorStyle, - hintText: genericDecoration.hintText, - contentPadding: genericDecoration.contentPadding, - border: genericDecoration.border, - focusedBorder: genericDecoration.focusedBorder, - suffixIcon: IconButton( - icon: Icon( - obscurePasswordInput ? Icons.visibility : Icons.visibility_off, - ), - onPressed: toggleObscurePasswordInput, - color: Colors.white, - )); + hintStyle: genericDecoration.hintStyle, + errorStyle: genericDecoration.errorStyle, + hintText: genericDecoration.hintText, + contentPadding: genericDecoration.contentPadding, + border: genericDecoration.border, + focusedBorder: genericDecoration.focusedBorder, + suffixIcon: IconButton( + icon: Icon( + obscurePasswordInput ? Icons.visibility : Icons.visibility_off, + ), + onPressed: toggleObscurePasswordInput, + color: Colors.white, + ), + ); } /// Displays terms and conditions if the user is /// logging in for the first time. -createSafeLoginButton(BuildContext context) { +InkResponse createSafeLoginButton(BuildContext context) { return InkResponse( - onTap: () { - _showLoginDetails(context); - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - child: Container( - padding: const EdgeInsets.all(8), - child: const Text( - '''Ao entrares confirmas que concordas com estes Termos e Condições''', - textAlign: TextAlign.center, - style: TextStyle( - decoration: TextDecoration.underline, - color: Colors.white, - fontSize: 17.0, - fontWeight: FontWeight.w300), - ))); + onTap: () { + _showLoginDetails(context); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(8), + child: const Text( + '''Ao entrares confirmas que concordas com estes Termos e Condições''', + textAlign: TextAlign.center, + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w300, + ), + ), + ), + ); } /// Displays 'Terms and conditions' section. Future _showLoginDetails(BuildContext context) async { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Termos e Condições'), - content: const SingleChildScrollView(child: TermsAndConditions()), - actions: [ - SimpleDialogOption( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ) - ], - ); - }); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Termos e Condições'), + content: const SingleChildScrollView(child: TermsAndConditions()), + actions: [ + SimpleDialogOption( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ) + ], + ); + }, + ); } diff --git a/uni/lib/view/logout_route.dart b/uni/lib/view/logout_route.dart index aa40787f0..ef0e269ac 100644 --- a/uni/lib/view/logout_route.dart +++ b/uni/lib/view/logout_route.dart @@ -5,10 +5,12 @@ import 'package:uni/view/login/login.dart'; class LogoutRoute { LogoutRoute._(); - static MaterialPageRoute buildLogoutRoute() { - return MaterialPageRoute(builder: (context) { - logout(context); - return const LoginPageView(); - }); + static MaterialPageRoute buildLogoutRoute() { + return MaterialPageRoute( + builder: (context) { + logout(context); + return const LoginPageView(); + }, + ); } } diff --git a/uni/lib/view/navigation_service.dart b/uni/lib/view/navigation_service.dart index 233faccf9..bad444ac3 100644 --- a/uni/lib/view/navigation_service.dart +++ b/uni/lib/view/navigation_service.dart @@ -6,8 +6,10 @@ class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); - static logout() { + static void logout() { navigatorKey.currentState?.pushNamedAndRemoveUntil( - '/${DrawerItem.navLogOut.title}', (_) => false); + '/${DrawerItem.navLogOut.title}', + (_) => false, + ); } } diff --git a/uni/lib/view/profile/profile.dart b/uni/lib/view/profile/profile.dart index 864ad4459..8545ab231 100644 --- a/uni/lib/view/profile/profile.dart +++ b/uni/lib/view/profile/profile.dart @@ -9,7 +9,7 @@ import 'package:uni/view/profile/widgets/course_info_card.dart'; import 'package:uni/view/profile/widgets/profile_overview.dart'; class ProfilePageView extends StatefulWidget { - const ProfilePageView({Key? key}) : super(key: key); + const ProfilePageView({super.key}); @override State createState() => ProfilePageViewState(); @@ -22,24 +22,30 @@ class ProfilePageViewState extends SecondaryPageViewState { return LazyConsumer( builder: (context, profileStateProvider) { final profile = profileStateProvider.profile; - final List courseWidgets = profile.courses - .map((e) => [ - CourseInfoCard(course: e), - const Padding(padding: EdgeInsets.all(5.0)) - ]) + final courseWidgets = profile.courses + .map( + (e) => [ + CourseInfoCard(course: e), + const Padding(padding: EdgeInsets.all(5)) + ], + ) .flattened .toList(); - return ListView(shrinkWrap: false, children: [ - const Padding(padding: EdgeInsets.all(5.0)), - ProfileOverview( + return ListView( + children: [ + const Padding(padding: EdgeInsets.all(5)), + ProfileOverview( profile: profile, - getProfileDecorationImage: getProfileDecorationImage), - const Padding(padding: EdgeInsets.all(5.0)), - // PrintInfoCard() // TODO: Bring this back when print info is ready again - ...courseWidgets, - AccountInfoCard(), - ]); + getProfileDecorationImage: getProfileDecorationImage, + ), + const Padding(padding: EdgeInsets.all(5)), + // TODO(bdmendes): Bring this back when print info is ready again + // PrintInfoCard() + ...courseWidgets, + AccountInfoCard(), + ], + ); }, ); } diff --git a/uni/lib/view/profile/widgets/account_info_card.dart b/uni/lib/view/profile/widgets/account_info_card.dart index 5e262ec20..7a2bbb148 100644 --- a/uni/lib/view/profile/widgets/account_info_card.dart +++ b/uni/lib/view/profile/widgets/account_info_card.dart @@ -12,11 +12,13 @@ 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) class AccountInfoCard extends GenericCard { - AccountInfoCard({Key? key}) : super(key: key); + AccountInfoCard({super.key}); const AccountInfoCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override void onRefresh(BuildContext context) { @@ -28,84 +30,133 @@ class AccountInfoCard extends GenericCard { @override Widget buildCardContent(BuildContext context) { return LazyConsumer( - builder: (context, profileStateProvider) { - return LazyConsumer( + builder: (context, profileStateProvider) { + return LazyConsumer( builder: (context, referenceProvider) { - final profile = profileStateProvider.profile; - final List references = referenceProvider.references; + final profile = profileStateProvider.profile; + final List references = referenceProvider.references; - return Column(children: [ - Table( - columnWidths: const {1: FractionColumnWidth(.4)}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, + return Column( children: [ - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 20.0, bottom: 8.0, left: 20.0), - child: Text('Saldo: ', - style: Theme.of(context).textTheme.titleSmall), + 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( + 'Saldo: ', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + 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, + bottom: 20, + left: 20, + ), + child: Text( + 'Data limite próxima prestação: ', + 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!) + : 'Sem data', + context, + ), + ) + ], + ), + TableRow( + children: [ + Container( + margin: const EdgeInsets.only( + top: 8, + bottom: 20, + left: 20, + ), + child: Text( + 'Notificar próxima data limite: ', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Container( + margin: const EdgeInsets.only( + top: 8, + bottom: 20, + left: 20, + ), + child: const TuitionNotificationSwitch(), + ) + ], + ) + ], + ), + Container( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Text( + 'Referências pendentes', + style: Theme.of(context).textTheme.titleLarge?.apply( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], ), - Container( - margin: const EdgeInsets.only( - top: 20.0, bottom: 8.0, right: 30.0), - child: getInfoText(profile.feesBalance, context)) - ]), - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: Text('Data limite próxima prestação: ', - style: Theme.of(context).textTheme.titleSmall), - ), - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, right: 30.0), - child: getInfoText( - profile.feesLimit != null - ? DateFormat('yyyy-MM-dd') - .format(profile.feesLimit!) - : 'Sem data', - context)) - ]), - TableRow(children: [ - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: Text("Notificar próxima data limite: ", - style: Theme.of(context).textTheme.titleSmall)), - Container( - margin: const EdgeInsets.only( - top: 8.0, bottom: 20.0, left: 20.0), - child: const TuitionNotificationSwitch()) - ]) - ]), - Container( - padding: const EdgeInsets.all(10), - child: Row(children: [ - Text('Referências pendentes', - style: Theme.of(context).textTheme.titleLarge?.apply( - color: Theme.of(context).colorScheme.secondary)), - ])), - ReferenceList(references: references), - const SizedBox(height: 10), - showLastRefreshedTime(profileStateProvider.feesRefreshTime, context) - ]); - }); - }); + ), + ReferenceList(references: references), + const SizedBox(height: 10), + showLastRefreshedTime( + profileStateProvider.lastUpdateTime?.toIso8601String(), + context, + ) + ], + ); + }, + ); + }, + ); } @override String getTitle() => 'Conta Corrente'; @override - onClick(BuildContext context) {} + void onClick(BuildContext context) {} } class ReferenceList extends StatelessWidget { - final List references; + const ReferenceList({required this.references, super.key}); - const ReferenceList({Key? key, required this.references}) : super(key: key); + final List references; @override Widget build(BuildContext context) { @@ -113,7 +164,7 @@ class ReferenceList extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Text( - "Não existem referências a pagar", + 'Não existem referências a pagar', style: Theme.of(context).textTheme.titleSmall, textScaleFactor: 0.96, ), @@ -122,14 +173,16 @@ class ReferenceList extends StatelessWidget { if (references.length == 1) { return ReferenceSection(reference: references[0]); } - return Column(children: [ - ReferenceSection(reference: references[0]), - const Divider( - thickness: 1, - indent: 30, - endIndent: 30, - ), - ReferenceSection(reference: references[1]), - ]); + return Column( + children: [ + ReferenceSection(reference: references[0]), + const Divider( + thickness: 1, + indent: 30, + endIndent: 30, + ), + ReferenceSection(reference: references[1]), + ], + ); } } diff --git a/uni/lib/view/profile/widgets/course_info_card.dart b/uni/lib/view/profile/widgets/course_info_card.dart index 8b7eeeb27..55d0eb890 100644 --- a/uni/lib/view/profile/widgets/course_info_card.dart +++ b/uni/lib/view/profile/widgets/course_info_card.dart @@ -5,95 +5,121 @@ import 'package:uni/view/common_widgets/generic_card.dart'; /// Manages the courses info (course name, atual year, state and year of /// first enrolment) on the user personal page. class CourseInfoCard extends GenericCard { - CourseInfoCard({Key? key, required this.course}) : super(key: key); + CourseInfoCard({required this.course, super.key}); final Course course; @override Widget buildCardContent(BuildContext context) { return Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow(children: [ + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 20.0), - child: Text('Ano curricular atual: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 20, bottom: 8, left: 20), + child: Text( + 'Ano curricular atual: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 20.0, bottom: 8.0, right: 20.0), + margin: const EdgeInsets.only(top: 20, bottom: 8, right: 20), child: getInfoText(course.currYear ?? 'Indisponível', context), ) - ]), - TableRow(children: [ + ], + ), + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: Text('Estado atual: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 10, bottom: 8, left: 20), + child: Text( + 'Estado atual: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), + margin: const EdgeInsets.only(top: 10, bottom: 8, right: 20), child: getInfoText(course.state ?? 'Indisponível', context), ) - ]), - TableRow(children: [ + ], + ), + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: Text('Ano da primeira inscrição: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 10, bottom: 8, left: 20), + child: Text( + 'Ano da primeira inscrição: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), - child: getInfoText( - course.firstEnrollment != null - ? '${course.firstEnrollment}/${course.firstEnrollment! + 1}' - : '?', - context)) - ]), - TableRow(children: [ + margin: const EdgeInsets.only(top: 10, bottom: 8, right: 20), + child: getInfoText( + course.firstEnrollment != null + ? '${course.firstEnrollment}/${course.firstEnrollment! + 1}' + : '?', + context, + ), + ) + ], + ), + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: Text('Faculdade: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 10, bottom: 8, left: 20), + child: Text( + 'Faculdade: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), - child: getInfoText( - course.faculty?.toUpperCase() ?? 'Indisponível', context)) - ]), - TableRow(children: [ + margin: const EdgeInsets.only(top: 10, bottom: 8, right: 20), + child: getInfoText( + course.faculty?.toUpperCase() ?? 'Indisponível', + context, + ), + ) + ], + ), + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 10.0, bottom: 8.0, left: 20.0), - child: Text('Média: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 10, bottom: 8, left: 20), + child: Text( + 'Média: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 8.0, right: 20.0), - child: getInfoText( - course.currentAverage?.toString() ?? 'Indisponível', - context)) - ]), - TableRow(children: [ + margin: const EdgeInsets.only(top: 10, bottom: 8, right: 20), + child: getInfoText( + course.currentAverage?.toString() ?? 'Indisponível', + context, + ), + ) + ], + ), + TableRow( + children: [ Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, left: 20.0), - child: Text('ECTs realizados: ', - style: Theme.of(context).textTheme.titleSmall), + margin: const EdgeInsets.only(top: 10, bottom: 20, left: 20), + child: Text( + 'ECTs realizados: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: - const EdgeInsets.only(top: 10.0, bottom: 20.0, right: 20.0), - child: getInfoText( - course.finishedEcts?.toString().replaceFirst('.0', '') ?? - '?', - context)) - ]) - ]); + margin: const EdgeInsets.only(top: 10, bottom: 20, right: 20), + child: getInfoText( + course.finishedEcts?.toString().replaceFirst('.0', '') ?? '?', + context, + ), + ) + ], + ) + ], + ); } @override @@ -102,7 +128,7 @@ class CourseInfoCard extends GenericCard { } @override - onClick(BuildContext context) {} + void onClick(BuildContext context) {} @override void onRefresh(BuildContext context) {} diff --git a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart index 5ce0adf4b..f2fb513e5 100644 --- a/uni/lib/view/profile/widgets/create_print_mb_dialog.dart +++ b/uni/lib/view/profile/widgets/create_print_mb_dialog.dart @@ -6,16 +6,16 @@ import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; Future addMoneyDialog(BuildContext context) async { - final GlobalKey formKey = GlobalKey(); - final TextEditingController controller = - TextEditingController(text: '1,00 €'); + final formKey = GlobalKey(); + final controller = TextEditingController(text: '1,00 €'); return showDialog( - context: context, - builder: (BuildContext context) { - double value = 1.00; + context: context, + builder: (BuildContext context) { + var value = 1.0; - return StatefulBuilder(builder: (context, setState) { + return StatefulBuilder( + builder: (context, setState) { void onValueChange() { final inputValue = valueTextToNumber(controller.text); setState(() => value = inputValue); @@ -25,17 +25,22 @@ Future addMoneyDialog(BuildContext context) async { return AlertDialog( content: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 5, bottom: 10), - child: Text( - 'Os dados da referência gerada aparecerão no Sigarra, conta corrente. \nPerfil > Conta Corrente', - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.titleSmall)), - Row(children: [ + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 5, bottom: 10), + child: Text( + 'Os dados da referência gerada aparecerão no Sigarra, ' + 'conta corrente. \n' + 'Perfil > Conta Corrente', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + Row( + children: [ IconButton( icon: const Icon(Icons.indeterminate_check_box), tooltip: 'Decrementar 1,00€', @@ -45,13 +50,16 @@ Future addMoneyDialog(BuildContext context) async { if (decreasedValue < 1) return; controller.value = TextEditingValue( - text: numberToValueText(decreasedValue)); + text: numberToValueText(decreasedValue), + ); }, ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 10), + horizontal: 5, + vertical: 10, + ), child: TextFormField( controller: controller, inputFormatters: [formatter], @@ -59,15 +67,16 @@ Future addMoneyDialog(BuildContext context) async { textAlign: TextAlign.center, onTap: () { controller.value = const TextEditingValue( - text: '', - selection: - TextSelection.collapsed(offset: 0)); + selection: TextSelection.collapsed(offset: 0), + ); }, onChanged: (string) { controller.value = TextEditingValue( - text: string, - selection: TextSelection.collapsed( - offset: string.length)); + text: string, + selection: TextSelection.collapsed( + offset: string.length, + ), + ); }, ), ), @@ -77,28 +86,39 @@ Future addMoneyDialog(BuildContext context) async { tooltip: 'Incrementar 1,00€', onPressed: () { controller.value = TextEditingValue( - text: numberToValueText( - valueTextToNumber(controller.text) + 1)); + text: numberToValueText( + valueTextToNumber(controller.text) + 1, + ), + ); }, ) - ]) - ], - )), - title: Text('Adicionar quota', - style: Theme.of(context).textTheme.headlineSmall), + ], + ) + ], + ), + ), + title: Text( + 'Adicionar quota', + style: Theme.of(context).textTheme.headlineSmall, + ), actions: [ TextButton( - child: Text('Cancelar', - style: Theme.of(context).textTheme.bodyMedium), - onPressed: () => Navigator.pop(context)), + child: Text( + 'Cancelar', + style: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () => Navigator.pop(context), + ), ElevatedButton( onPressed: () => generateReference(context, value), child: const Text('Gerar referência'), ) ], ); - }); - }); + }, + ); + }, + ); } final CurrencyTextInputFormatter formatter = @@ -110,9 +130,9 @@ double valueTextToNumber(String value) => String numberToValueText(double number) => formatter.format(number.toStringAsFixed(2)); -void generateReference(context, amount) async { +Future generateReference(BuildContext context, double amount) async { if (amount < 1) { - ToastMessage.warning(context, 'Valor mínimo: 1,00 €'); + await ToastMessage.warning(context, 'Valor mínimo: 1,00 €'); return; } @@ -120,10 +140,10 @@ void generateReference(context, amount) async { final response = await PrintFetcher.generatePrintMoneyReference(amount, session); - if (response.statusCode == 200) { + if (response.statusCode == 200 && context.mounted) { Navigator.of(context).pop(false); - ToastMessage.success(context, 'Referência criada com sucesso!'); + await ToastMessage.success(context, 'Referência criada com sucesso!'); } else { - ToastMessage.error(context, 'Algum erro!'); + await ToastMessage.error(context, 'Algum erro!'); } } diff --git a/uni/lib/view/profile/widgets/print_info_card.dart b/uni/lib/view/profile/widgets/print_info_card.dart index d53c38bbb..b7bee2fbe 100644 --- a/uni/lib/view/profile/widgets/print_info_card.dart +++ b/uni/lib/view/profile/widgets/print_info_card.dart @@ -6,11 +6,13 @@ import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/profile/widgets/create_print_mb_dialog.dart'; class PrintInfoCard extends GenericCard { - PrintInfoCard({Key? key}) : super(key: key); + PrintInfoCard({super.key}); const PrintInfoCard.fromEditingInformation( - Key key, bool editingMode, Function()? onDelete) - : super.fromEditingInformation(key, editingMode, onDelete); + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); @override Widget buildCardContent(BuildContext context) { @@ -21,38 +23,52 @@ class PrintInfoCard extends GenericCard { mainAxisSize: MainAxisSize.min, children: [ Table( - columnWidths: const { - 1: FractionColumnWidth(0.4), - 2: FractionColumnWidth(.1) - }, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow(children: [ + columnWidths: const { + 1: FractionColumnWidth(0.4), + 2: FractionColumnWidth(.1) + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ Container( margin: const EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 20.0), - child: Text('Valor disponível: ', - style: Theme.of(context).textTheme.titleSmall), + top: 20, + bottom: 20, + left: 20, + ), + child: Text( + 'Valor disponível: ', + style: Theme.of(context).textTheme.titleSmall, + ), ), Container( - margin: const EdgeInsets.only(right: 15.0), - child: Text(profile.printBalance, - textAlign: TextAlign.end, - style: Theme.of(context).textTheme.titleLarge)), + 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.0), - height: 30, - child: ElevatedButton( - style: OutlinedButton.styleFrom( - padding: EdgeInsets.zero, - ), - onPressed: () => addMoneyDialog(context), - child: const Center(child: Icon(Icons.add)), - )), - ]) - ]), + 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.printRefreshTime, context) + profileStateProvider.lastUpdateTime?.toIso8601String(), + context, + ) ], ); }, @@ -63,7 +79,7 @@ class PrintInfoCard extends GenericCard { String getTitle() => 'Impressões'; @override - onClick(BuildContext context) {} + void onClick(BuildContext context) {} @override void onRefresh(BuildContext context) { diff --git a/uni/lib/view/profile/widgets/profile_overview.dart b/uni/lib/view/profile/widgets/profile_overview.dart index d313e3a38..2ea9b1f30 100644 --- a/uni/lib/view/profile/widgets/profile_overview.dart +++ b/uni/lib/view/profile/widgets/profile_overview.dart @@ -7,42 +7,52 @@ import 'package:uni/model/providers/startup/profile_provider.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; class ProfileOverview extends StatelessWidget { + const ProfileOverview({ + required this.profile, + required this.getProfileDecorationImage, + super.key, + }); final Profile profile; final DecorationImage Function(File?) getProfileDecorationImage; - const ProfileOverview( - {Key? key, - required this.profile, - required this.getProfileDecorationImage}) - : super(key: key); - @override Widget build(BuildContext context) { return Consumer( builder: (context, sessionProvider, _) { return FutureBuilder( future: ProfileProvider.fetchOrGetCachedProfilePicture( - null, sessionProvider.session), + sessionProvider.session, + ), builder: (BuildContext context, AsyncSnapshot profilePic) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - width: 150.0, - height: 150.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: getProfileDecorationImage(profilePic.data))), - const Padding(padding: EdgeInsets.all(8.0)), - Text(profile.name, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.w400)), - const Padding(padding: EdgeInsets.all(5.0)), - Text(profile.email, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18.0, fontWeight: FontWeight.w300)), + 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, + ), + ), ], ), ); diff --git a/uni/lib/view/profile/widgets/reference_section.dart b/uni/lib/view/profile/widgets/reference_section.dart index d32628460..d59b942f9 100644 --- a/uni/lib/view/profile/widgets/reference_section.dart +++ b/uni/lib/view/profile/widgets/reference_section.dart @@ -5,56 +5,60 @@ import 'package:uni/model/entities/reference.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; class ReferenceSection extends StatelessWidget { + const ReferenceSection({required this.reference, super.key}); final Reference reference; - const ReferenceSection({Key? key, required this.reference}) : super(key: key); - @override Widget build(BuildContext context) { - return Column(children: [ - TitleText(title: reference.description), - InfoCopyRow( + return Column( + children: [ + TitleText(title: reference.description), + InfoCopyRow( infoName: 'Entidade', info: reference.entity.toString(), - copyMessage: 'Entidade copiada!'), - InfoCopyRow( + copyMessage: 'Entidade copiada!', + ), + InfoCopyRow( infoName: 'Referência', info: reference.reference.toString(), - copyMessage: 'Referência copiada!'), - InfoCopyRow( + copyMessage: 'Referência copiada!', + ), + InfoCopyRow( infoName: 'Montante', info: reference.amount.toString(), copyMessage: 'Montante copiado!', - isMoney: true), - ]); + isMoney: true, + ), + ], + ); } } class InfoText extends StatelessWidget { + const InfoText({required this.text, this.color, super.key}); final String text; final Color? color; - const InfoText({Key? key, required this.text, this.color}) : super(key: key); - @override Widget build(BuildContext context) { return Text( text, textScaleFactor: 0.9, - style: Theme.of(context).textTheme.titleSmall?.copyWith(color: color), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: color, + ), ); } } class TitleText extends StatelessWidget { + const TitleText({required this.title, super.key}); final String title; - const TitleText({Key? key, required this.title}) : super(key: key); - @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 20.0), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 20), alignment: Alignment.centerLeft, child: Text( title, @@ -67,29 +71,27 @@ class TitleText extends StatelessWidget { } class InfoCopyRow extends StatelessWidget { + const InfoCopyRow({ + required this.infoName, + required this.info, + required this.copyMessage, + super.key, + this.isMoney = false, + }); final String infoName; final String info; final String copyMessage; final bool isMoney; - const InfoCopyRow( - {Key? key, - required this.infoName, - required this.info, - required this.copyMessage, - this.isMoney = false}) - : super(key: key); - @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 20.0), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 20), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ InfoText(text: infoName), const Spacer(), - InfoText(text: "${isMoney ? _getMoneyAmount() : info} "), + InfoText(text: '${isMoney ? _getMoneyAmount() : info} '), InkWell( splashColor: Theme.of(context).highlightColor, child: const Icon(Icons.content_copy, size: 16), diff --git a/uni/lib/view/profile/widgets/tuition_notification_switch.dart b/uni/lib/view/profile/widgets/tuition_notification_switch.dart index ec7f776b9..9abd7766f 100644 --- a/uni/lib/view/profile/widgets/tuition_notification_switch.dart +++ b/uni/lib/view/profile/widgets/tuition_notification_switch.dart @@ -17,13 +17,13 @@ class _TuitionNotificationSwitchState extends State { getTuitionNotificationToggle(); } - getTuitionNotificationToggle() async { - AppSharedPreferences.getTuitionNotificationToggle() + Future getTuitionNotificationToggle() async { + await AppSharedPreferences.getTuitionNotificationToggle() .then((value) => setState(() => tuitionNotificationToggle = value)); } - saveTuitionNotificationToggle(bool value) async { - await AppSharedPreferences.setTuitionNotificationToggle(value); + Future saveTuitionNotificationToggle({required bool value}) async { + await AppSharedPreferences.setTuitionNotificationToggle(value: value); setState(() { tuitionNotificationToggle = value; }); @@ -32,9 +32,8 @@ class _TuitionNotificationSwitchState extends State { @override Widget build(BuildContext context) { return Switch.adaptive( - value: tuitionNotificationToggle, - onChanged: (value) { - saveTuitionNotificationToggle(value); - }); + value: tuitionNotificationToggle, + onChanged: (value) => saveTuitionNotificationToggle(value: value), + ); } } diff --git a/uni/lib/view/restaurant/restaurant_page_view.dart b/uni/lib/view/restaurant/restaurant_page_view.dart index 2ff5bd339..55b7ad0e7 100644 --- a/uni/lib/view/restaurant/restaurant_page_view.dart +++ b/uni/lib/view/restaurant/restaurant_page_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/model/utils/day_of_week.dart'; @@ -12,7 +11,7 @@ import 'package:uni/view/restaurant/widgets/restaurant_page_card.dart'; import 'package:uni/view/restaurant/widgets/restaurant_slot.dart'; class RestaurantPageView extends StatefulWidget { - const RestaurantPageView({Key? key}) : super(key: key); + const RestaurantPageView({super.key}); @override State createState() => _RestaurantPageState(); @@ -27,71 +26,90 @@ class _RestaurantPageState extends GeneralPageViewState @override void initState() { super.initState(); - final int weekDay = DateTime.now().weekday; + final weekDay = DateTime.now().weekday; super.initState(); tabController = TabController(vsync: this, length: DayOfWeek.values.length); - tabController.animateTo((tabController.index + (weekDay - 1))); + tabController.animateTo(tabController.index + (weekDay - 1)); scrollViewController = ScrollController(); } @override Widget getBody(BuildContext context) { return LazyConsumer( - builder: (context, restaurantProvider) { - return Column(children: [ - ListView(scrollDirection: Axis.vertical, shrinkWrap: true, children: [ - Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), - alignment: Alignment.center, - child: const PageTitle(name: 'Ementas', center: false, pad: false), - ), - TabBar( - controller: tabController, - isScrollable: true, - tabs: createTabs(context), - ), - ]), - const SizedBox(height: 10), - RequestDependentWidgetBuilder( - status: restaurantProvider.status, - builder: () => - createTabViewBuilder(restaurantProvider.restaurants, context), - hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, - onNullContent: - const Center(child: Text('Não há refeições disponíveis.'))) - ]); - }); + builder: (context, restaurantProvider) { + return Column( + children: [ + ListView( + shrinkWrap: true, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), + alignment: Alignment.center, + child: const PageTitle( + name: 'Ementas', + center: false, + pad: false, + ), + ), + TabBar( + controller: tabController, + isScrollable: true, + tabs: createTabs(context), + ), + ], + ), + const SizedBox(height: 10), + RequestDependentWidgetBuilder( + status: restaurantProvider.status, + builder: () => + createTabViewBuilder(restaurantProvider.restaurants, context), + hasContentPredicate: restaurantProvider.restaurants.isNotEmpty, + onNullContent: + const Center(child: Text('Não há refeições disponíveis.')), + ) + ], + ); + }, + ); } Widget createTabViewBuilder(dynamic restaurants, BuildContext context) { final List dayContents = DayOfWeek.values.map((dayOfWeek) { - List restaurantsWidgets = []; + var restaurantsWidgets = []; if (restaurants is List) { restaurantsWidgets = restaurants - .map((restaurant) => RestaurantPageCard(restaurant.name, - RestaurantDay(restaurant: restaurant, day: dayOfWeek))) + .map( + (restaurant) => RestaurantPageCard( + restaurant.name, + RestaurantDay(restaurant: restaurant, day: dayOfWeek), + ), + ) .toList(); } return ListView(children: restaurantsWidgets); }).toList(); return Expanded( - child: TabBarView( - controller: tabController, - children: dayContents, - )); + child: TabBarView( + controller: tabController, + children: dayContents, + ), + ); } List createTabs(BuildContext context) { - final List tabs = []; + final tabs = []; for (var i = 0; i < DayOfWeek.values.length; i++) { - tabs.add(Container( - color: Theme.of(context).colorScheme.background, - child: Tab( + tabs.add( + ColoredBox( + color: Theme.of(context).colorScheme.background, + child: Tab( key: Key('cantine-page-tab-$i'), - text: toString(DayOfWeek.values[i])), - )); + text: toString(DayOfWeek.values[i]), + ), + ), + ); } return tabs; @@ -105,37 +123,38 @@ class _RestaurantPageState extends GeneralPageViewState } class RestaurantDay extends StatelessWidget { + const RestaurantDay({required this.restaurant, required this.day, super.key}); final Restaurant restaurant; final DayOfWeek day; - const RestaurantDay({Key? key, required this.restaurant, required this.day}) - : super(key: key); - @override Widget build(BuildContext context) { - final List meals = restaurant.getMealsOfDay(day); + final meals = restaurant.getMealsOfDay(day); if (meals.isEmpty) { return Container( - margin: const EdgeInsets.only(top: 10, bottom: 5), - key: Key('restaurant-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - SizedBox(height: 10), - Center( - child: Text("Não há informação disponível sobre refeições")), - ], - )); + margin: const EdgeInsets.only(top: 10, bottom: 5), + key: Key('restaurant-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + SizedBox(height: 10), + Center( + child: Text('Não há informação disponível sobre refeições'), + ), + ], + ), + ); } else { return Container( - margin: const EdgeInsets.only(top: 5, bottom: 5), - key: Key('restaurant-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: meals - .map((meal) => RestaurantSlot(type: meal.type, name: meal.name)) - .toList(), - )); + margin: const EdgeInsets.only(top: 5, bottom: 5), + key: Key('restaurant-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: meals + .map((meal) => RestaurantSlot(type: meal.type, name: meal.name)) + .toList(), + ), + ); } } } diff --git a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart index ca2fae3e4..a3e5c3dc9 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_page_card.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_page_card.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:uni/view/common_widgets/generic_card.dart'; class RestaurantPageCard extends GenericCard { - final String restaurantName; - final Widget meals; - RestaurantPageCard(this.restaurantName, this.meals, {super.key}) : super.customStyle( - editingMode: false, onDelete: () => null, hasSmallTitle: true); + editingMode: false, + onDelete: () {}, + hasSmallTitle: true, + ); + final String restaurantName; + final Widget meals; @override Widget buildCardContent(BuildContext context) { @@ -20,7 +22,7 @@ class RestaurantPageCard extends GenericCard { } @override - onClick(BuildContext context) {} + void onClick(BuildContext context) {} @override void onRefresh(BuildContext context) {} diff --git a/uni/lib/view/restaurant/widgets/restaurant_slot.dart b/uni/lib/view/restaurant/widgets/restaurant_slot.dart index 564e62ae2..d1f57cef4 100644 --- a/uni/lib/view/restaurant/widgets/restaurant_slot.dart +++ b/uni/lib/view/restaurant/widgets/restaurant_slot.dart @@ -2,43 +2,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class RestaurantSlot extends StatelessWidget { - final String type; - final String name; - const RestaurantSlot({ - Key? key, required this.type, required this.name, - }) : super(key: key); + super.key, + }); + final String type; + final String name; @override Widget build(BuildContext context) { return Container( - padding: - const EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10, right: 22.0), + padding: const EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 22), child: Container( - key: Key('cantine-slot-type-$type'), - child: Row( - children: [ - Container( - margin: const EdgeInsets.fromLTRB(0, 0, 8.0, 0), - child: SizedBox( - width: 20, - child: RestaurantSlotType(type: type), - )), - Flexible( - child: Text( + key: Key('cantine-slot-type-$type'), + child: Row( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(0, 0, 8, 0), + child: SizedBox( + width: 20, + child: RestaurantSlotType(type: type), + ), + ), + Flexible( + child: Text( name, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.left, - )) - ], - )), + ), + ) + ], + ), + ), ); } } class RestaurantSlotType extends StatelessWidget { + const RestaurantSlotType({required this.type, super.key}); final String type; static const mealTypeIcons = { @@ -50,25 +52,28 @@ class RestaurantSlotType extends StatelessWidget { 'salada': 'assets/meal-icons/salad.svg', }; - const RestaurantSlotType({Key? key, required this.type}) : super(key: key); - @override Widget build(BuildContext context) { - final String icon = getIcon(); + final icon = getIcon(); return Tooltip( - message: type, - child: icon != '' - ? SvgPicture.asset( - icon, - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn), - height: 20, - ) - : null); + message: type, + child: icon != '' + ? SvgPicture.asset( + icon, + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, + ), + height: 20, + ) + : null, + ); } String getIcon() => mealTypeIcons.entries - .firstWhere((element) => type.toLowerCase().contains(element.key), - orElse: () => const MapEntry('', '')) + .firstWhere( + (element) => type.toLowerCase().contains(element.key), + orElse: () => const MapEntry('', ''), + ) .value; } diff --git a/uni/lib/view/schedule/schedule.dart b/uni/lib/view/schedule/schedule.dart index bb6800c51..fd146c54d 100644 --- a/uni/lib/view/schedule/schedule.dart +++ b/uni/lib/view/schedule/schedule.dart @@ -5,15 +5,15 @@ import 'package:uni/model/entities/time_utilities.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/request_status.dart'; import 'package:uni/utils/drawer_items.dart'; +import 'package:uni/view/common_widgets/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/lazy_consumer.dart'; -import 'package:uni/view/common_widgets/expanded_image_label.dart'; import 'package:uni/view/schedule/widgets/schedule_slot.dart'; class SchedulePage extends StatefulWidget { - const SchedulePage({Key? key}) : super(key: key); + const SchedulePage({super.key}); @override SchedulePageState createState() => SchedulePageState(); @@ -35,11 +35,13 @@ class SchedulePageState extends State { /// Manages the 'schedule' sections of the app class SchedulePageView extends StatefulWidget { - SchedulePageView( - {Key? key, required this.lectures, required this.scheduleStatus}) - : super(key: key); + SchedulePageView({ + required this.lectures, + required this.scheduleStatus, + super.key, + }); - final List? lectures; + final List lectures; final RequestStatus? scheduleStatus; final int weekDay = DateTime.now().weekday; @@ -47,12 +49,12 @@ class SchedulePageView extends StatefulWidget { static final List daysOfTheWeek = TimeString.getWeekdaysStrings(includeWeekend: false); - static List> groupLecturesByDay(schedule) { + static List> groupLecturesByDay(List schedule) { final aggLectures = >[]; - for (int i = 0; i < daysOfTheWeek.length; i++) { - final Set lectures = {}; - for (int j = 0; j < schedule.length; j++) { + for (var i = 0; i < daysOfTheWeek.length; 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); @@ -72,11 +74,13 @@ class SchedulePageViewState extends GeneralPageViewState void initState() { super.initState(); tabController = TabController( - vsync: this, length: SchedulePageView.daysOfTheWeek.length); + vsync: this, + length: SchedulePageView.daysOfTheWeek.length, + ); final offset = (widget.weekDay > 5) ? 0 : (widget.weekDay - 1) % SchedulePageView.daysOfTheWeek.length; - tabController?.animateTo((tabController!.index + offset)); + tabController?.animateTo(tabController!.index + offset); } @override @@ -87,49 +91,57 @@ class SchedulePageViewState extends GeneralPageViewState @override Widget getBody(BuildContext context) { - final MediaQueryData queryData = MediaQuery.of(context); - - return Column(children: [ - ListView( - scrollDirection: Axis.vertical, - shrinkWrap: true, - children: [ - PageTitle(name: DrawerItem.navSchedule.title), - TabBar( + final queryData = MediaQuery.of(context); + + return Column( + children: [ + ListView( + shrinkWrap: true, + children: [ + PageTitle(name: DrawerItem.navSchedule.title), + TabBar( + controller: tabController, + isScrollable: true, + physics: const BouncingScrollPhysics(), + tabs: createTabs(queryData, context), + ), + ], + ), + Expanded( + child: TabBarView( controller: tabController, - isScrollable: true, - physics: const BouncingScrollPhysics(), - tabs: createTabs(queryData, context), + children: + createSchedule(context, widget.lectures, widget.scheduleStatus), ), - ], - ), - Expanded( - child: TabBarView( - controller: tabController, - children: - createSchedule(context, widget.lectures, widget.scheduleStatus), - )) - ]); + ) + ], + ); } /// Returns a list of widgets empty with tabs for each day of the week. - List createTabs(queryData, BuildContext context) { - final List tabs = []; + List createTabs(MediaQueryData queryData, BuildContext context) { + final tabs = []; for (var i = 0; i < SchedulePageView.daysOfTheWeek.length; i++) { - tabs.add(SizedBox( - width: queryData.size.width * 1 / 4, - child: Tab( + tabs.add( + SizedBox( + width: queryData.size.width * 1 / 4, + child: Tab( key: Key('schedule-page-tab-$i'), - text: SchedulePageView.daysOfTheWeek[i]), - )); + text: SchedulePageView.daysOfTheWeek[i], + ), + ), + ); } return tabs; } List createSchedule( - context, List? lectures, RequestStatus? scheduleStatus) { - final List tabBarViewContent = []; - for (int i = 0; i < SchedulePageView.daysOfTheWeek.length; i++) { + BuildContext context, + List lectures, + RequestStatus? scheduleStatus, + ) { + final tabBarViewContent = []; + for (var i = 0; i < SchedulePageView.daysOfTheWeek.length; i++) { tabBarViewContent .add(createScheduleByDay(context, i, lectures, scheduleStatus)); } @@ -137,47 +149,60 @@ class SchedulePageViewState extends GeneralPageViewState } /// Returns a list of widgets for the rows with a singular class info. - List createScheduleRows(lectures, BuildContext context) { - final List scheduleContent = []; - lectures = lectures.toList(); - for (int i = 0; i < lectures.length; i++) { - final Lecture lecture = lectures[i]; - scheduleContent.add(ScheduleSlot( - subject: lecture.subject, - typeClass: lecture.typeClass, - rooms: lecture.room, - begin: lecture.startTime, - end: lecture.endTime, - occurrId: lecture.occurrId, - teacher: lecture.teacher, - classNumber: lecture.classNumber, - )); + 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, dayContent, BuildContext context) { + Widget dayColumnBuilder( + int day, + Set dayContent, + BuildContext context, + ) { return Container( - key: Key('schedule-page-day-column-$day'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: createScheduleRows(dayContent, context), - )); + key: Key('schedule-page-day-column-$day'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: createScheduleRows(dayContent, context), + ), + ); } - Widget createScheduleByDay(BuildContext context, int day, - List? lectures, RequestStatus? scheduleStatus) { - final List aggLectures = SchedulePageView.groupLecturesByDay(lectures); + Widget createScheduleByDay( + BuildContext context, + int day, + List lectures, + RequestStatus? scheduleStatus, + ) { + final aggLectures = SchedulePageView.groupLecturesByDay(lectures); return RequestDependentWidgetBuilder( - status: scheduleStatus ?? RequestStatus.none, - builder: () => dayColumnBuilder(day, aggLectures[day], context), - hasContentPredicate: aggLectures[day].isNotEmpty, - onNullContent: Center( - child: ImageLabel( + status: scheduleStatus ?? RequestStatus.none, + builder: () => dayColumnBuilder(day, aggLectures[day], context), + hasContentPredicate: aggLectures[day].isNotEmpty, + onNullContent: Center( + child: ImageLabel( imagePath: 'assets/images/schedule.png', label: 'Não possui aulas à ${SchedulePageView.daysOfTheWeek[day]}.', labelTextStyle: const TextStyle(fontSize: 15), - ))); + ), + ), + ); } @override diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index c6571076b..d63957f02 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -5,17 +5,7 @@ import 'package:uni/view/common_widgets/row_container.dart'; import 'package:url_launcher/url_launcher.dart'; class ScheduleSlot extends StatelessWidget { - final String subject; - final String rooms; - final DateTime begin; - final DateTime end; - final String teacher; - final String typeClass; - final String? classNumber; - final int occurrId; - const ScheduleSlot({ - Key? key, required this.subject, required this.typeClass, required this.rooms, @@ -24,85 +14,107 @@ class ScheduleSlot extends StatelessWidget { required this.occurrId, required this.teacher, this.classNumber, - }) : super(key: key); + super.key, + }); + final String subject; + final String rooms; + final DateTime begin; + final DateTime end; + final String teacher; + final String typeClass; + final String? classNumber; + final int occurrId; @override Widget build(BuildContext context) { return RowContainer( - child: Container( - padding: const EdgeInsets.only( - top: 10.0, bottom: 10.0, left: 22.0, right: 22.0), child: Container( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 22, + right: 22, + ), + child: Container( key: Key( - 'schedule-slot-time-${DateFormat("HH:mm").format(begin)}-${DateFormat("HH:mm").format(end)}'), - margin: const EdgeInsets.only(top: 3.0, bottom: 3.0), + 'schedule-slot-time-${DateFormat("HH:mm").format(begin)}-' + '${DateFormat("HH:mm").format(end)}', + ), + margin: const EdgeInsets.only(top: 3, bottom: 3), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: createScheduleSlotPrimInfo(context), - )), - )); + ), + ), + ), + ); } - List createScheduleSlotPrimInfo(context) { + List createScheduleSlotPrimInfo(BuildContext context) { final subjectTextField = TextFieldWidget( - text: subject, - style: Theme.of(context) - .textTheme - .headlineSmall! - .apply(color: Theme.of(context).colorScheme.tertiary), - alignment: TextAlign.center); + text: subject, + style: Theme.of(context) + .textTheme + .headlineSmall! + .apply(color: Theme.of(context).colorScheme.tertiary), + alignment: TextAlign.center, + ); final typeClassTextField = TextFieldWidget( - text: ' ($typeClass)', - style: Theme.of(context).textTheme.bodyMedium, - alignment: TextAlign.center); + text: ' ($typeClass)', + style: Theme.of(context).textTheme.bodyMedium, + alignment: TextAlign.center, + ); final roomTextField = TextFieldWidget( - text: rooms, - style: Theme.of(context).textTheme.bodyMedium, - alignment: TextAlign.right); + text: rooms, + style: Theme.of(context).textTheme.bodyMedium, + alignment: TextAlign.right, + ); return [ ScheduleTimeWidget( - begin: DateFormat("HH:mm").format(begin), - end: DateFormat("HH:mm").format(end)), + begin: DateFormat('HH:mm').format(begin), + end: DateFormat('HH:mm').format(end), + ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SubjectButtonWidget( - occurrId: occurrId, - ), - subjectTextField, - typeClassTextField, - ], - ), - Padding( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SubjectButtonWidget( + occurrId: occurrId, + ), + subjectTextField, + typeClassTextField, + ], + ), + Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ScheduleTeacherClassInfoWidget( - classNumber: classNumber, teacher: teacher)), - ], - )), + classNumber: classNumber, + teacher: teacher, + ), + ), + ], + ), + ), roomTextField ]; } } class SubjectButtonWidget extends StatelessWidget { + const SubjectButtonWidget({required this.occurrId, super.key}); final int occurrId; - const SubjectButtonWidget({super.key, required this.occurrId}); - String toUcLink(int occurrId) { - const String faculty = 'feup'; // should not be hardcoded + const faculty = 'feup'; // should not be hardcoded return '${NetworkRouter.getBaseUrl(faculty)}' 'UCURR_GERAL.FICHA_UC_VIEW?pv_ocorrencia_id=$occurrId'; } Future _launchURL() async { - final String url = toUcLink(occurrId); + final url = toUcLink(occurrId); await launchUrl(Uri.parse(url)); } @@ -113,8 +125,9 @@ class SubjectButtonWidget extends StatelessWidget { children: [ IconButton( constraints: const BoxConstraints( - minHeight: kMinInteractiveDimension / 3, - minWidth: kMinInteractiveDimension / 3), + minHeight: kMinInteractiveDimension / 3, + minWidth: kMinInteractiveDimension / 3, + ), icon: const Icon(Icons.open_in_browser), iconSize: 18, color: Colors.grey, @@ -128,12 +141,14 @@ class SubjectButtonWidget extends StatelessWidget { } class ScheduleTeacherClassInfoWidget extends StatelessWidget { + const ScheduleTeacherClassInfoWidget({ + required this.teacher, + this.classNumber, + super.key, + }); final String? classNumber; final String teacher; - const ScheduleTeacherClassInfoWidget( - {super.key, required this.teacher, this.classNumber}); - @override Widget build(BuildContext context) { return TextFieldWidget( @@ -145,11 +160,10 @@ class ScheduleTeacherClassInfoWidget extends StatelessWidget { } class ScheduleTimeWidget extends StatelessWidget { + const ScheduleTimeWidget({required this.begin, required this.end, super.key}); final String begin; final String end; - const ScheduleTimeWidget({super.key, required this.begin, required this.end}); - @override Widget build(BuildContext context) { return Column( @@ -163,12 +177,14 @@ class ScheduleTimeWidget extends StatelessWidget { } class ScheduleTimeTextField extends StatelessWidget { + const ScheduleTimeTextField({ + required this.time, + required this.context, + super.key, + }); final String time; final BuildContext context; - const ScheduleTimeTextField( - {super.key, required this.time, required this.context}); - @override Widget build(BuildContext context) { return TextFieldWidget( @@ -180,16 +196,15 @@ class ScheduleTimeTextField extends StatelessWidget { } class TextFieldWidget extends StatelessWidget { - final String text; - final TextStyle? style; - final TextAlign alignment; - const TextFieldWidget({ - super.key, required this.text, required this.style, required this.alignment, + super.key, }); + final String text; + final TextStyle? style; + final TextAlign alignment; @override Widget build(BuildContext context) { diff --git a/uni/lib/view/splash/splash.dart b/uni/lib/view/splash/splash.dart index 2496143a4..20651033e 100644 --- a/uni/lib/view/splash/splash.dart +++ b/uni/lib/view/splash/splash.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:tuple/tuple.dart'; import 'package:uni/controller/load_static/terms_and_conditions.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/model/providers/state_providers.dart'; @@ -13,7 +12,7 @@ import 'package:uni/view/splash/widgets/terms_and_condition_dialog.dart'; import 'package:uni/view/theme.dart'; class SplashScreen extends StatefulWidget { - const SplashScreen({Key? key}) : super(key: key); + const SplashScreen({super.key}); @override SplashScreenState createState() => SplashScreenState(); @@ -40,54 +39,65 @@ class SplashScreenState extends State { : applicationLightTheme; return Theme( - data: systemTheme, - child: Builder( - builder: (context) => Scaffold( - body: Stack( - fit: StackFit.expand, + data: systemTheme, + child: Builder( + builder: (context) => Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: const BoxDecoration(), + ), + Center( + child: createTitle(context), + ), + Column( + children: [ + const Spacer(), + Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - decoration: const BoxDecoration(), + const CircularProgressIndicator(), + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 16, + ), ), - Center( - child: createTitle(context), - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Spacer(), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 16)), - createNILogo(context), - ], - ), - Padding( - padding: EdgeInsets.only( - bottom: queryData.size.height / 15)) - ], - ) + createNILogo(context), ], ), - ))); + Padding( + padding: EdgeInsets.only( + bottom: queryData.size.height / 15, + ), + ) + ], + ) + ], + ), + ), + ), + ); } /// Creates the app Title container with the app's logo. Widget createTitle(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints( - minWidth: queryData.size.width / 8, - minHeight: queryData.size.height / 6, + constraints: BoxConstraints( + minWidth: queryData.size.width / 8, + minHeight: queryData.size.height / 6, + ), + child: SizedBox( + width: 150, + child: SvgPicture.asset( + 'assets/images/logo_dark.svg', + colorFilter: ColorFilter.mode( + Theme.of(context).primaryColor, + BlendMode.srcIn, + ), ), - child: SizedBox( - width: 150.0, - child: SvgPicture.asset('assets/images/logo_dark.svg', - colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, BlendMode.srcIn)))); + ), + ); } /// Creates the app main logo @@ -101,11 +111,11 @@ class SplashScreenState extends State { } // Redirects the user to the proper page depending on his login input. - void changeRouteAccordingToLoginAndTerms() async { - final Tuple2 userPersistentInfo = + Future changeRouteAccordingToLoginAndTerms() async { + final userPersistentInfo = await AppSharedPreferences.getPersistentUserInfo(); - final String userName = userPersistentInfo.item1; - final String password = userPersistentInfo.item2; + final userName = userPersistentInfo.item1; + final password = userPersistentInfo.item2; MaterialPageRoute nextRoute; if (userName != '' && password != '') { @@ -118,20 +128,25 @@ class SplashScreenState extends State { } if (mounted) { - Navigator.pushReplacement(context, nextRoute); + unawaited(Navigator.pushReplacement(context, nextRoute)); } } - Future termsAndConditionsRoute( - String userName, String password, StateProviders stateProviders) async { + Future> termsAndConditionsRoute( + String userName, + String password, + StateProviders stateProviders, + ) async { final termsAcceptance = await TermsAndConditionDialog.buildIfTermsChanged( - context, userName, password); + context, + userName, + password, + ); switch (termsAcceptance) { case TermsAndConditionsState.accepted: if (mounted) { - final List faculties = - await AppSharedPreferences.getUserFaculties(); + final faculties = await AppSharedPreferences.getUserFaculties(); stateProviders.sessionProvider .restoreSession(userName, password, faculties); } diff --git a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart index 719b002ab..a7b36393f 100644 --- a/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart +++ b/uni/lib/view/splash/widgets/terms_and_condition_dialog.dart @@ -12,14 +12,19 @@ class TermsAndConditionDialog { TermsAndConditionDialog._(); static Future buildIfTermsChanged( - BuildContext context, String userName, String password) async { + BuildContext context, + String userName, + String password, + ) async { final termsAreAccepted = await updateTermsAndConditionsAcceptancePreference(); if (!termsAreAccepted) { final routeCompleter = Completer(); - SchedulerBinding.instance.addPostFrameCallback((timestamp) => - _buildShowDialog(context, routeCompleter, userName, password)); + SchedulerBinding.instance.addPostFrameCallback( + (timestamp) => + _buildShowDialog(context, routeCompleter, userName, password), + ); return routeCompleter.future; } @@ -27,58 +32,62 @@ class TermsAndConditionDialog { } static Future _buildShowDialog( - BuildContext context, - Completer routeCompleter, - String userName, - String password) { + BuildContext context, + Completer routeCompleter, + String userName, + String password, + ) { return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Mudança nos Termos e Condições da uni', - style: Theme.of(context).textTheme.headlineSmall), - content: Column( - children: [ - const Expanded( - child: SingleChildScrollView(child: TermsAndConditions()), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - routeCompleter - .complete(TermsAndConditionsState.accepted); - await AppSharedPreferences - .setTermsAndConditionsAcceptance(true); - }, - child: const Text( - 'Aceito', - )), - const SizedBox( - width: 10, + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Mudança nos Termos e Condições da uni', + style: Theme.of(context).textTheme.headlineSmall, + ), + content: Column( + children: [ + const Expanded( + child: SingleChildScrollView(child: TermsAndConditions()), + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + routeCompleter.complete(TermsAndConditionsState.accepted); + await AppSharedPreferences + .setTermsAndConditionsAcceptance(areAccepted: true); + }, + child: const Text( + 'Aceito', ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - routeCompleter - .complete(TermsAndConditionsState.rejected); - await AppSharedPreferences - .setTermsAndConditionsAcceptance(false); - }, - child: const Text( - 'Rejeito', - )), - ], - ) - ], - ), - ); - }); + ), + const SizedBox( + width: 10, + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + routeCompleter.complete(TermsAndConditionsState.rejected); + await AppSharedPreferences + .setTermsAndConditionsAcceptance(areAccepted: false); + }, + child: const Text( + 'Rejeito', + ), + ), + ], + ) + ], + ), + ); + }, + ); } } diff --git a/uni/lib/view/theme.dart b/uni/lib/view/theme.dart index 985509e79..1a444ec77 100644 --- a/uni/lib/view/theme.dart +++ b/uni/lib/view/theme.dart @@ -11,109 +11,113 @@ const Color _darkishBlack = Color.fromARGB(255, 43, 43, 43); const Color _darkBlack = Color.fromARGB(255, 27, 27, 27); const _textTheme = TextTheme( - displayLarge: TextStyle(fontSize: 40.0, fontWeight: FontWeight.w400), - displayMedium: TextStyle(fontSize: 32.0, fontWeight: FontWeight.w400), - displaySmall: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w400), - headlineMedium: TextStyle(fontSize: 24.0, fontWeight: FontWeight.w300), - headlineSmall: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w400), - titleLarge: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300), - titleMedium: TextStyle(fontSize: 17.0, fontWeight: FontWeight.w300), - titleSmall: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w300), - bodyLarge: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400), - bodyMedium: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - bodySmall: TextStyle(fontSize: 13.0, fontWeight: FontWeight.w400), + displayLarge: TextStyle(fontSize: 40, fontWeight: FontWeight.w400), + displayMedium: TextStyle(fontSize: 32, fontWeight: FontWeight.w400), + displaySmall: TextStyle(fontSize: 28, fontWeight: FontWeight.w400), + headlineMedium: TextStyle(fontSize: 24, fontWeight: FontWeight.w300), + headlineSmall: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), + titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w300), + titleMedium: TextStyle(fontSize: 17, fontWeight: FontWeight.w300), + titleSmall: TextStyle(fontSize: 16, fontWeight: FontWeight.w300), + bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400), + bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400), + bodySmall: TextStyle(fontSize: 13, fontWeight: FontWeight.w400), ); ThemeData applicationLightTheme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: darkRed, - brightness: Brightness.light, - background: _mildWhite, - primary: darkRed, - onPrimary: Colors.white, - secondary: darkRed, - onSecondary: Colors.white, - 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, - indicatorColor: darkRed, - primaryTextTheme: Typography().black.copyWith( + colorScheme: ColorScheme.fromSeed( + seedColor: darkRed, + background: _mildWhite, + primary: darkRed, + onPrimary: Colors.white, + secondary: darkRed, + onSecondary: Colors.white, + 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, + indicatorColor: darkRed, + primaryTextTheme: Typography().black.copyWith( headlineMedium: const TextStyle(color: _strongGrey), - bodyLarge: const TextStyle(color: _strongGrey)), - 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, + bodyLarge: const TextStyle(color: _strongGrey), ), + iconTheme: const IconThemeData(color: darkRed), + textTheme: _textTheme, + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.resolveWith( + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), + trackColor: MaterialStateProperty.resolveWith( + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, ), - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.selected) ? darkRed : null, - ), - )); + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, + ), + ), + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (Set states) => + states.contains(MaterialState.selected) ? darkRed : null, + ), + ), +); ThemeData applicationDarkTheme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: lightRed, - brightness: Brightness.dark, - background: _darkBlack, - primary: _lightGrey, - onPrimary: _darkishBlack, - secondary: _lightGrey, - onSecondary: _darkishBlack, - tertiary: _lightGrey, - onTertiary: _darkishBlack), + colorScheme: ColorScheme.fromSeed( + seedColor: lightRed, brightness: Brightness.dark, - textSelectionTheme: const TextSelectionThemeData( - selectionHandleColor: Colors.transparent, + background: _darkBlack, + primary: _lightGrey, + onPrimary: _darkishBlack, + secondary: _lightGrey, + 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, + 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, ), - primaryColor: _lightGrey, - canvasColor: _darkBlack, - scaffoldBackgroundColor: _darkBlack, - cardColor: _mildBlack, - hintColor: _darkishBlack, - dividerColor: _strongGrey, - 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, ), - 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, ), - 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 626a55ba8..0ce1ffd5a 100644 --- a/uni/lib/view/theme_notifier.dart +++ b/uni/lib/view/theme_notifier.dart @@ -6,14 +6,14 @@ class ThemeNotifier with ChangeNotifier { ThemeMode _themeMode; - getTheme() => _themeMode; + ThemeMode getTheme() => _themeMode; - setNextTheme() { + void setNextTheme() { final nextThemeMode = (_themeMode.index + 1) % 3; setTheme(ThemeMode.values[nextThemeMode]); } - setTheme(ThemeMode themeMode) { + void setTheme(ThemeMode themeMode) { _themeMode = themeMode; AppSharedPreferences.setThemeMode(themeMode); notifyListeners(); diff --git a/uni/lib/view/useful_info/useful_info.dart b/uni/lib/view/useful_info/useful_info.dart index c575a105c..1989b73d2 100644 --- a/uni/lib/view/useful_info/useful_info.dart +++ b/uni/lib/view/useful_info/useful_info.dart @@ -20,22 +20,25 @@ class UsefulInfoPageView extends StatefulWidget { 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() - ]); + 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.0), - child: const PageTitle(name: 'Úteis')); + padding: const EdgeInsets.only(bottom: 6), + child: const PageTitle(name: 'Úteis'), + ); } @override diff --git a/uni/lib/view/useful_info/widgets/academic_services_card.dart b/uni/lib/view/useful_info/widgets/academic_services_card.dart index 2b7ffc0be..452725278 100644 --- a/uni/lib/view/useful_info/widgets/academic_services_card.dart +++ b/uni/lib/view/useful_info/widgets/academic_services_card.dart @@ -3,7 +3,7 @@ import 'package:uni/view/common_widgets/generic_expansion_card.dart'; import 'package:uni/view/useful_info/widgets/text_components.dart'; class AcademicServicesCard extends GenericExpansionCard { - const AcademicServicesCard({Key? key}) : super(key: key); + const AcademicServicesCard({super.key}); @override Widget buildCardContent(BuildContext context) { @@ -15,8 +15,12 @@ class AcademicServicesCard extends GenericExpansionCard { h2('Atendimento telefónico', context), infoText('9:30h - 12:00h | 14:00h - 16:00h', context), h1('Telefone', context), - infoText('+351 225 081 977', context, - link: 'tel:225 081 977', last: true), + infoText( + '+351 225 081 977', + context, + link: 'tel:225 081 977', + last: true, + ), ], ); } diff --git a/uni/lib/view/useful_info/widgets/copy_center_card.dart b/uni/lib/view/useful_info/widgets/copy_center_card.dart index 62124f7d3..8a48026f7 100644 --- a/uni/lib/view/useful_info/widgets/copy_center_card.dart +++ b/uni/lib/view/useful_info/widgets/copy_center_card.dart @@ -3,7 +3,7 @@ import 'package:uni/view/common_widgets/generic_expansion_card.dart'; import 'package:uni/view/useful_info/widgets/text_components.dart'; class CopyCenterCard extends GenericExpansionCard { - const CopyCenterCard({Key? key}) : super(key: key); + const CopyCenterCard({super.key}); @override Widget buildCardContent(BuildContext context) { @@ -18,8 +18,12 @@ class CopyCenterCard extends GenericExpansionCard { h2('AEFEUP ', context), infoText('+351 220 994 132', context, link: 'tel:220 994 132'), h1('Email', context), - infoText('editorial@aefeup.pt', context, - link: 'mailto:editorial@aefeup.pt', last: true) + infoText( + 'editorial@aefeup.pt', + context, + link: 'mailto:editorial@aefeup.pt', + last: true, + ) ], ); } diff --git a/uni/lib/view/useful_info/widgets/dona_bia_card.dart b/uni/lib/view/useful_info/widgets/dona_bia_card.dart index fc1abd773..f7b2f5408 100644 --- a/uni/lib/view/useful_info/widgets/dona_bia_card.dart +++ b/uni/lib/view/useful_info/widgets/dona_bia_card.dart @@ -3,7 +3,7 @@ import 'package:uni/view/common_widgets/generic_expansion_card.dart'; import 'package:uni/view/useful_info/widgets/text_components.dart'; class DonaBiaCard extends GenericExpansionCard { - const DonaBiaCard({Key? key}) : super(key: key); + const DonaBiaCard({super.key}); @override Widget buildCardContent(BuildContext context) { @@ -15,8 +15,12 @@ class DonaBiaCard extends GenericExpansionCard { h1('Telefone', context), infoText('+351 225 081 416', context, link: 'tel:225 081 416'), h1('Email', context), - infoText('papelaria.fe.up@gmail.com', context, - last: true, link: 'mailto:papelaria.fe.up@gmail.com') + infoText( + 'papelaria.fe.up@gmail.com', + context, + last: true, + link: 'mailto:papelaria.fe.up@gmail.com', + ) ], ); } diff --git a/uni/lib/view/useful_info/widgets/infodesk_card.dart b/uni/lib/view/useful_info/widgets/infodesk_card.dart index 66cf8dac6..7574239b4 100644 --- a/uni/lib/view/useful_info/widgets/infodesk_card.dart +++ b/uni/lib/view/useful_info/widgets/infodesk_card.dart @@ -3,7 +3,7 @@ import 'package:uni/view/common_widgets/generic_expansion_card.dart'; import 'package:uni/view/useful_info/widgets/text_components.dart'; class InfoDeskCard extends GenericExpansionCard { - const InfoDeskCard({Key? key}) : super(key: key); + const InfoDeskCard({super.key}); @override Widget buildCardContent(BuildContext context) { @@ -15,8 +15,12 @@ class InfoDeskCard extends GenericExpansionCard { h1('Telefone', context), infoText('+351 225 081 400', context, link: 'tel:225 081 400'), h1('Email', context), - infoText('infodesk@fe.up.pt', context, - last: true, link: 'mailto:infodesk@fe.up.pt') + infoText( + 'infodesk@fe.up.pt', + context, + last: true, + link: 'mailto:infodesk@fe.up.pt', + ) ], ); } diff --git a/uni/lib/view/useful_info/widgets/link_button.dart b/uni/lib/view/useful_info/widgets/link_button.dart index f333eaa00..3dc240d3c 100644 --- a/uni/lib/view/useful_info/widgets/link_button.dart +++ b/uni/lib/view/useful_info/widgets/link_button.dart @@ -2,31 +2,37 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class LinkButton extends StatelessWidget { - final String title; - final String link; const LinkButton({ - Key? key, required this.title, required this.link, - }) : super(key: key); + super.key, + }); + final String title; + final String link; @override Widget build(BuildContext context) { return Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow(children: [ + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ Container( - margin: const EdgeInsets.only(top: 0, bottom: 14.0, left: 20.0), - child: InkWell( - child: Text(title, - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith(decoration: TextDecoration.underline)), - onTap: () => launchUrl(Uri.parse(link)), - )) - ]), - ]); + margin: const EdgeInsets.only(bottom: 14, left: 20), + child: InkWell( + child: Text( + title, + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith(decoration: TextDecoration.underline), + ), + onTap: () => launchUrl(Uri.parse(link)), + ), + ) + ], + ), + ], + ); } } diff --git a/uni/lib/view/useful_info/widgets/multimedia_center_card.dart b/uni/lib/view/useful_info/widgets/multimedia_center_card.dart index ce667f641..d0bdc7e72 100644 --- a/uni/lib/view/useful_info/widgets/multimedia_center_card.dart +++ b/uni/lib/view/useful_info/widgets/multimedia_center_card.dart @@ -3,7 +3,7 @@ import 'package:uni/view/common_widgets/generic_expansion_card.dart'; import 'package:uni/view/useful_info/widgets/text_components.dart'; class MultimediaCenterCard extends GenericExpansionCard { - const MultimediaCenterCard({Key? key}) : super(key: key); + const MultimediaCenterCard({super.key}); @override Widget buildCardContent(BuildContext context) { @@ -15,8 +15,12 @@ class MultimediaCenterCard extends GenericExpansionCard { h1('Telefone', context), infoText('+351 225 081 466', context, link: 'tel:225 081 466'), h1('Email', context), - infoText('imprimir@fe.up.pt', context, - last: true, link: 'mailto:imprimir@fe.up.pt') + infoText( + 'imprimir@fe.up.pt', + context, + last: true, + link: 'mailto:imprimir@fe.up.pt', + ) ], ); } diff --git a/uni/lib/view/useful_info/widgets/other_links_card.dart b/uni/lib/view/useful_info/widgets/other_links_card.dart index 92561b92d..651d614db 100644 --- a/uni/lib/view/useful_info/widgets/other_links_card.dart +++ b/uni/lib/view/useful_info/widgets/other_links_card.dart @@ -5,13 +5,15 @@ import 'package:uni/view/useful_info/widgets/link_button.dart'; /// Manages the 'Current account' section inside the user's page (accessible /// through the top-right widget with the user picture) class OtherLinksCard extends GenericExpansionCard { - const OtherLinksCard({Key? key}) : super(key: key); + const OtherLinksCard({super.key}); @override Widget buildCardContent(BuildContext context) { - return Column(children: const [ - LinkButton(title: 'Impressão', link: 'https://print.up.pt') - ]); + return Column( + children: const [ + LinkButton(title: 'Impressão', link: 'https://print.up.pt') + ], + ); } @override diff --git a/uni/lib/view/useful_info/widgets/sigarra_links_card.dart b/uni/lib/view/useful_info/widgets/sigarra_links_card.dart index 90fcbe3d0..48b5fbe3a 100644 --- a/uni/lib/view/useful_info/widgets/sigarra_links_card.dart +++ b/uni/lib/view/useful_info/widgets/sigarra_links_card.dart @@ -5,33 +5,41 @@ import 'package:uni/view/useful_info/widgets/link_button.dart'; /// Manages the 'Current account' section inside the user's page (accessible /// through the top-right widget with the user picture) class SigarraLinksCard extends GenericExpansionCard { - const SigarraLinksCard({Key? key}) : super(key: key); + const SigarraLinksCard({super.key}); @override Widget buildCardContent(BuildContext context) { - return Column(children: const [ - LinkButton( + return Column( + children: const [ + LinkButton( title: 'Notícias', - link: 'https://sigarra.up.pt/feup/pt/noticias_geral.lista_noticias'), - LinkButton( + link: 'https://sigarra.up.pt/feup/pt/noticias_geral.lista_noticias', + ), + LinkButton( title: 'Erasmus', link: - 'https://sigarra.up.pt/feup/pt/web_base.gera_pagina?P_pagina=257769'), - LinkButton( + 'https://sigarra.up.pt/feup/pt/web_base.gera_pagina?P_pagina=257769', + ), + LinkButton( title: 'Inscrição Geral', - link: 'https://sigarra.up.pt/feup/pt/ins_geral.inscricao'), - LinkButton( + link: 'https://sigarra.up.pt/feup/pt/ins_geral.inscricao', + ), + LinkButton( title: 'Inscrição de Turmas', - link: 'https://sigarra.up.pt/feup/pt/it_geral.ver_insc'), - LinkButton( + link: 'https://sigarra.up.pt/feup/pt/it_geral.ver_insc', + ), + LinkButton( title: 'Inscrição para Melhoria', link: - 'https://sigarra.up.pt/feup/pt/inqueritos_geral.inqueritos_list'), - LinkButton( + 'https://sigarra.up.pt/feup/pt/inqueritos_geral.inqueritos_list', + ), + LinkButton( title: 'Calendário Escolar', link: - 'https://sigarra.up.pt/feup/pt/web_base.gera_pagina?p_pagina=p%c3%a1gina%20est%c3%a1tica%20gen%c3%a9rica%20106') - ]); + 'https://sigarra.up.pt/feup/pt/web_base.gera_pagina?p_pagina=p%c3%a1gina%20est%c3%a1tica%20gen%c3%a9rica%20106', + ) + ], + ); } @override diff --git a/uni/lib/view/useful_info/widgets/text_components.dart b/uni/lib/view/useful_info/widgets/text_components.dart index 4858559cf..345685277 100644 --- a/uni/lib/view/useful_info/widgets/text_components.dart +++ b/uni/lib/view/useful_info/widgets/text_components.dart @@ -2,42 +2,50 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; Container h1(String text, BuildContext context, {bool initial = false}) { - final double marginTop = initial ? 15.0 : 30.0; + final marginTop = initial ? 15.0 : 30.0; return Container( - margin: EdgeInsets.only(top: marginTop, bottom: 0.0, left: 20.0), - child: Align( - alignment: Alignment.centerLeft, - child: Opacity( - opacity: 0.8, - child: - Text(text, style: Theme.of(context).textTheme.headlineSmall)), - )); + margin: EdgeInsets.only(top: marginTop, left: 20), + child: Align( + alignment: Alignment.centerLeft, + child: Opacity( + opacity: 0.8, + child: Text(text, style: Theme.of(context).textTheme.headlineSmall), + ), + ), + ); } Container h2(String text, BuildContext context) { return Container( - margin: const EdgeInsets.only(top: 13.0, bottom: 0.0, left: 20.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text(text, style: Theme.of(context).textTheme.titleSmall), - )); + margin: const EdgeInsets.only(top: 13, left: 20), + child: Align( + alignment: Alignment.centerLeft, + child: Text(text, style: Theme.of(context).textTheme.titleSmall), + ), + ); } -Container infoText(String text, BuildContext context, - {bool last = false, link = ''}) { - final double marginBottom = last ? 8.0 : 0.0; +Container infoText( + String text, + BuildContext context, { + bool last = false, + String link = '', +}) { + final marginBottom = last ? 8.0 : 0.0; return Container( - margin: EdgeInsets.only(top: 8, bottom: marginBottom, left: 20.0), - child: Align( - alignment: Alignment.centerLeft, - child: InkWell( - child: Text( - text, - style: Theme.of(context) - .textTheme - .bodyLarge! - .apply(color: Theme.of(context).colorScheme.tertiary), - ), - onTap: () => link != '' ? launchUrl(Uri.parse(link)) : null), - )); + margin: EdgeInsets.only(top: 8, bottom: marginBottom, left: 20), + child: Align( + alignment: Alignment.centerLeft, + child: InkWell( + child: Text( + text, + style: Theme.of(context) + .textTheme + .bodyLarge! + .apply(color: Theme.of(context).colorScheme.tertiary), + ), + onTap: () => link != '' ? launchUrl(Uri.parse(link)) : null, + ), + ), + ); } diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index 7719e308c..7ec0ebb55 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -1,20 +1,7 @@ name: uni description: A UP no teu bolso. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +publish_to: 'none' # We do not publish to pub.dev # The app version name is automatically bumped by CI. # To change it manually, override the value in app_version.txt. @@ -26,82 +13,66 @@ environment: sdk: ">=2.17.1 <3.0.0" flutter: 3.7.2 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. +# Dependencies specify other packages that the application needs in order to work. +# Major versions and critical security upgrades are managed by dependabot, and +# should not be changed manually without a good reason. dependencies: - flutter: - sdk: flutter - html: ^0.15.0 - http: ^0.13.0 - tuple: ^2.0.0 - shared_preferences: ^2.0.3 - provider: ^6.0.4 - encrypt: ^5.0.0-beta.1 - path_provider: ^2.0.0 - sqflite: ^2.0.3 - path: ^1.8.0 - flutter_svg: ^2.0.0+1 - synchronized: ^3.0.0 - image: ^4.0.13 + add_2_calendar: ^2.1.3 + cached_network_image: ^3.2.3 + collection: ^1.16.0 connectivity_plus: ^4.0.1 - logger: ^1.1.0 - url_launcher: ^6.0.2 - flutter_markdown: ^0.6.0 - intl: ^0.18.1 crypto: ^3.0.1 - add_2_calendar: ^2.1.3 - sentry_flutter: ^7.5.2 - email_validator: ^2.0.1 + cupertino_icons: ^1.0.2 currency_text_input_formatter: ^2.1.5 + email_validator: ^2.0.1 + encrypt: ^5.0.0-beta.1 expansion_tile_card: ^3.0.0 - collection: ^1.16.0 - timelines: ^0.1.0 + flutter: + sdk: flutter + flutter_dotenv: ^5.0.2 + flutter_local_notifications: ^15.1.0+1 flutter_map: ^4.0.0 - cached_network_image: ^3.2.3 - cupertino_icons: ^1.0.2 - latlong2: ^0.8.1 flutter_map_marker_popup: ^5.0.0 - workmanager: ^0.5.1 - flutter_local_notifications: ^15.1.0+1 - percent_indicator: ^4.2.2 + flutter_markdown: ^0.6.0 + flutter_svg: ^2.0.0+1 flutter_widget_from_html_core: ^0.10.3 - shimmer: ^3.0.0 + html: ^0.15.0 + http: ^0.13.0 + image: ^4.0.13 + intl: ^0.18.1 + latlong2: ^0.8.1 + logger: ^1.1.0 material_design_icons_flutter: ^7.0.7296 - flutter_dotenv: ^5.0.2 + path: ^1.8.0 + path_provider: ^2.0.0 + percent_indicator: ^4.2.2 + provider: ^6.0.4 + sentry_flutter: ^7.5.2 + shared_preferences: ^2.0.3 + 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 + workmanager: ^0.5.1 + dev_dependencies: + flutter_launcher_icons: ^0.13.1 flutter_test: sdk: flutter - test: any mockito: ^5.2.0 - flutter_launcher_icons: ^0.13.1 - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^2.0.0 + test: any + very_good_analysis: ^4.0.0+1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg assets: - assets/ - assets/images/ @@ -110,31 +81,6 @@ flutter: - assets/text/locations/ - assets/meal-icons/ - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom Fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages fonts: - family: Raleway fonts: @@ -165,5 +111,3 @@ flutter_icons: image_path: "assets/icon/icon.png" adaptive_icon_background: "#75171E" adaptive_icon_foreground: "assets/icon/android_icon_foreground.png" - - diff --git a/uni/test/analysis_options.yaml b/uni/test/analysis_options.yaml new file mode 100644 index 000000000..36d4692d5 --- /dev/null +++ b/uni/test/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../analysis_options.yaml + +linter: + rules: + enable_null_safety: false \ No newline at end of file diff --git a/uni/test/integration/src/exams_page_test.dart b/uni/test/integration/src/exams_page_test.dart index b975df0f5..c57ff9199 100644 --- a/uni/test/integration/src/exams_page_test.dart +++ b/uni/test/integration/src/exams_page_test.dart @@ -29,36 +29,37 @@ void main() { final mockClient = MockClient(); final mockResponse = MockResponse(); final sopeCourseUnit = CourseUnit( - abbreviation: 'SOPE', - occurrId: 0, - name: 'Sistemas Operativos', - status: 'V'); + abbreviation: 'SOPE', + occurrId: 0, + name: 'Sistemas Operativos', + status: 'V', + ); final sdisCourseUnit = CourseUnit( - abbreviation: 'SDIS', - name: 'Sistemas Distribuídos', - occurrId: 0, - status: 'V'); - - final DateTime beginSopeExam = DateTime.parse('2099-11-18 17:00'); - final DateTime endSopeExam = DateTime.parse('2099-11-18 19:00'); + abbreviation: 'SDIS', + name: 'Sistemas Distribuídos', + occurrId: 0, + status: 'V', + ); + + final beginSopeExam = DateTime.parse('2099-11-18 17:00'); + final endSopeExam = DateTime.parse('2099-11-18 19:00'); final sopeExam = Exam('44426', beginSopeExam, endSopeExam, 'SOPE', [], 'MT', 'feup'); - final DateTime beginSdisExam = DateTime.parse('2099-10-21 17:00'); - final DateTime endSdisExam = DateTime.parse('2099-10-21 19:00'); + final beginSdisExam = DateTime.parse('2099-10-21 17:00'); + final endSdisExam = DateTime.parse('2099-10-21 19:00'); final sdisExam = Exam('44425', beginSdisExam, endSdisExam, 'SDIS', [], 'MT', 'feup'); - final DateTime beginMdisExam = DateTime.parse('2099-10-22 17:00'); - final DateTime endMdisExam = DateTime.parse('2099-10-22 19:00'); + final beginMdisExam = DateTime.parse('2099-10-22 17:00'); + final endMdisExam = DateTime.parse('2099-10-22 19:00'); final mdisExam = Exam('44429', beginMdisExam, endMdisExam, 'MDIS', [], 'MT', 'feup'); - final Map filteredExams = {}; - for (String type in Exam.displayedTypes) { + final filteredExams = {}; + for (final type in Exam.displayedTypes) { filteredExams[type] = true; } - final profile = Profile(); - profile.courses = [Course(id: 7474, faculty: 'feup')]; + final profile = Profile()..courses = [Course(id: 7474, faculty: 'feup')]; testWidgets('Exams', (WidgetTester tester) async { NetworkRouter.httpClient = mockClient; @@ -83,11 +84,12 @@ void main() { expect(find.byKey(Key('$mdisExam-exam')), findsNothing); await examProvider.fetchUserExams( - ParserExams(), - const Tuple2('', ''), - profile, - Session(username: '', cookies: '', faculties: ['feup']), - [sopeCourseUnit, sdisCourseUnit]); + ParserExams(), + const Tuple2('', ''), + profile, + Session(username: '', cookies: '', faculties: ['feup']), + [sopeCourseUnit, sdisCourseUnit], + ); await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); @@ -118,11 +120,12 @@ void main() { expect(find.byKey(Key('$sopeExam-exam')), findsNothing); await examProvider.fetchUserExams( - ParserExams(), - const Tuple2('', ''), - profile, - Session(username: '', cookies: '', faculties: ['feup']), - [sopeCourseUnit, sdisCourseUnit]); + ParserExams(), + const Tuple2('', ''), + profile, + Session(username: '', cookies: '', faculties: ['feup']), + [sopeCourseUnit, sdisCourseUnit], + ); await tester.pumpAndSettle(); expect(find.byKey(Key('$sdisExam-exam')), findsOneWidget); @@ -143,11 +146,11 @@ void main() { expect(find.byType(AlertDialog), findsOneWidget); - final CheckboxListTile mtCheckboxTile = find + final mtCheckboxTile = find .byKey(const Key('ExamCheck' 'Mini-testes')) .evaluate() .first - .widget; + .widget as CheckboxListTile; expect(find.byWidget(mtCheckboxTile), findsOneWidget); expect(mtCheckboxTile.value, true); diff --git a/uni/test/integration/src/schedule_page_test.dart b/uni/test/integration/src/schedule_page_test.dart index 2ac6230c5..c01bf08e2 100644 --- a/uni/test/integration/src/schedule_page_test.dart +++ b/uni/test/integration/src/schedule_page_test.dart @@ -14,6 +14,7 @@ import 'package:uni/model/entities/course.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/schedule/schedule.dart'; import '../../test_widget.dart'; @@ -23,11 +24,13 @@ class MockClient extends Mock implements http.Client {} class MockResponse extends Mock implements http.Response {} +class MockSessionProvider extends Mock implements SessionProvider {} + class UriMatcher extends CustomMatcher { - UriMatcher(matcher) : super('Uri that has', 'string', matcher); + UriMatcher(Matcher matcher) : super('Uri that has', 'string', matcher); @override - Object featureValueOf(actual) => (actual as Uri).toString(); + Object featureValueOf(dynamic actual) => (actual as Uri).toString(); } void main() { @@ -39,19 +42,24 @@ void main() { const htmlFetcherIdentifier = 'hor_geral.estudantes_view'; const jsonFetcherIdentifier = 'mob_hor_geral.estudante'; - Future testSchedule(WidgetTester tester) async { - final profile = Profile(); - profile.courses = [Course(id: 7474)]; + Future testSchedule(WidgetTester tester) async { + final profile = Profile()..courses = [Course(id: 7474)]; NetworkRouter.httpClient = mockClient; when(badMockResponse.statusCode).thenReturn(500); final scheduleProvider = LectureProvider(); + final sessionProvider = MockSessionProvider(); + + when(sessionProvider.session).thenReturn( + Session(username: 'up1234', cookies: 'cookie', faculties: ['feup']), + ); const widget = SchedulePage(); final providers = [ ChangeNotifierProvider(create: (_) => scheduleProvider), + ChangeNotifierProvider(create: (_) => sessionProvider), ]; await tester.pumpWidget(testableWidget(widget, providers: providers)); @@ -62,8 +70,11 @@ void main() { expect(find.byKey(const Key(scheduleSlotTimeKey1)), findsNothing); expect(find.byKey(const Key(scheduleSlotTimeKey2)), findsNothing); - await scheduleProvider.fetchUserLectures(const Tuple2('', ''), - Session(username: '', cookies: '', faculties: ['feup']), profile); + await scheduleProvider.fetchUserLectures( + const Tuple2('', ''), + Session(username: '', cookies: '', faculties: ['feup']), + profile, + ); await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); @@ -88,13 +99,19 @@ void main() { .readAsStringSync(encoding: const Latin1Codec()); when(mockResponse.body).thenReturn(mockJson); when(mockResponse.statusCode).thenReturn(200); - when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => badMockResponse); - - when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => mockResponse); + when( + mockClient.get( + argThat(UriMatcher(contains(htmlFetcherIdentifier))), + headers: anyNamed('headers'), + ), + ).thenAnswer((_) async => badMockResponse); + + when( + mockClient.get( + argThat(UriMatcher(contains(jsonFetcherIdentifier))), + headers: anyNamed('headers'), + ), + ).thenAnswer((_) async => mockResponse); await testSchedule(tester); }); @@ -104,13 +121,19 @@ void main() { .readAsStringSync(encoding: const Latin1Codec()); when(mockResponse.body).thenReturn(mockHtml); when(mockResponse.statusCode).thenReturn(200); - when(mockClient.get(argThat(UriMatcher(contains(htmlFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => mockResponse); - - when(mockClient.get(argThat(UriMatcher(contains(jsonFetcherIdentifier))), - headers: anyNamed('headers'))) - .thenAnswer((_) async => badMockResponse); + when( + mockClient.get( + argThat(UriMatcher(contains(htmlFetcherIdentifier))), + headers: anyNamed('headers'), + ), + ).thenAnswer((_) async => mockResponse); + + when( + mockClient.get( + argThat(UriMatcher(contains(jsonFetcherIdentifier))), + headers: anyNamed('headers'), + ), + ).thenAnswer((_) async => badMockResponse); await testSchedule(tester); }); diff --git a/uni/test/test_widget.dart b/uni/test/test_widget.dart index 012869d06..c8430b967 100644 --- a/uni/test/test_widget.dart +++ b/uni/test/test_widget.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -Widget testableWidget(Widget widget, - {List providers = const []}) { +Widget testableWidget( + Widget widget, { + List providers = const [], +}) { if (providers.isEmpty) return wrapWidget(widget); return MultiProvider(providers: providers, child: wrapWidget(widget)); @@ -10,7 +12,8 @@ Widget testableWidget(Widget widget, Widget wrapWidget(Widget widget) { return MaterialApp( - home: Scaffold( - body: widget, - )); + home: Scaffold( + body: widget, + ), + ); } diff --git a/uni/test/unit/providers/exams_provider_test.dart b/uni/test/unit/providers/exams_provider_test.dart index 4aacd7a8d..2ac9bf387 100644 --- a/uni/test/unit/providers/exams_provider_test.dart +++ b/uni/test/unit/providers/exams_provider_test.dart @@ -21,30 +21,45 @@ void main() { final mockResponse = MockResponse(); final sopeCourseUnit = CourseUnit( - abbreviation: 'SOPE', - occurrId: 0, - name: 'Sistemas Operativos', - status: 'V'); + abbreviation: 'SOPE', + occurrId: 0, + name: 'Sistemas Operativos', + status: 'V', + ); final sdisCourseUnit = CourseUnit( - abbreviation: 'SDIS', - occurrId: 0, - name: 'Sistemas Distribuídos', - status: 'V'); - - final List rooms = ['B119', 'B107', 'B205']; - final DateTime beginSopeExam = DateTime.parse('2800-09-12 12:00'); - final DateTime endSopeExam = DateTime.parse('2800-09-12 15:00'); - final sopeExam = Exam('1229', beginSopeExam, endSopeExam, 'SOPE', rooms, - 'Recurso - Época Recurso (2ºS)', 'feup'); - final DateTime beginSdisExam = DateTime.parse('2800-09-12 12:00'); - final DateTime endSdisExam = DateTime.parse('2800-09-12 15:00'); - final sdisExam = Exam('1230', beginSdisExam, endSdisExam, 'SDIS', rooms, - 'Recurso - Época Recurso (2ºS)', 'feup'); - - const Tuple2 userPersistentInfo = Tuple2('', ''); - - final profile = Profile(); - profile.courses = [Course(id: 7474)]; + abbreviation: 'SDIS', + occurrId: 0, + name: 'Sistemas Distribuídos', + status: 'V', + ); + + final rooms = ['B119', 'B107', 'B205']; + final beginSopeExam = DateTime.parse('2800-09-12 12:00'); + final endSopeExam = DateTime.parse('2800-09-12 15:00'); + final sopeExam = Exam( + '1229', + beginSopeExam, + endSopeExam, + 'SOPE', + rooms, + 'Recurso - Época Recurso (2ºS)', + 'feup', + ); + final beginSdisExam = DateTime.parse('2800-09-12 12:00'); + final endSdisExam = DateTime.parse('2800-09-12 15:00'); + final sdisExam = Exam( + '1230', + beginSdisExam, + endSdisExam, + 'SDIS', + rooms, + 'Recurso - Época Recurso (2ºS)', + 'feup', + ); + + const userPersistentInfo = Tuple2('', ''); + + final profile = Profile()..courses = [Course(id: 7474)]; final session = Session(username: '', cookies: '', faculties: ['feup']); final userUcs = [sopeCourseUnit, sdisCourseUnit]; @@ -65,7 +80,12 @@ void main() { .thenAnswer((_) async => {sopeExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.exams.isNotEmpty, true); expect(provider.exams, [sopeExam]); @@ -77,30 +97,42 @@ void main() { .thenAnswer((_) async => {sopeExam, sdisExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); }); - test('''When given three exams but one is to be parsed out, + test(''' +When given three exams but one is to be parsed out, since it is a Special Season Exam''', () async { - final DateTime begin = DateTime.parse('2800-09-12 12:00'); - final DateTime end = DateTime.parse('2800-09-12 15:00'); + final begin = DateTime.parse('2800-09-12 12:00'); + final end = DateTime.parse('2800-09-12 15:00'); final specialExam = Exam( - '1231', - begin, - end, - 'SDIS', - rooms, - 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', - 'feup'); + '1231', + begin, + end, + 'SDIS', + rooms, + 'Exames ao abrigo de estatutos especiais - Port.Est.Especiais', + 'feup', + ); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {sopeExam, sdisExam, specialExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.successful); expect(provider.exams, [sopeExam, sdisExam]); @@ -111,54 +143,94 @@ void main() { .thenAnswer((_) async => throw Exception('RIP')); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); - + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.failed); }); test('When Exam is today in one hour', () async { - final DateTime begin = DateTime.now().add(const Duration(hours: 1)); - final DateTime end = DateTime.now().add(const Duration(hours: 2)); - final todayExam = Exam('1232', begin, end, 'SDIS', rooms, - 'Recurso - Época Recurso (1ºS)', 'feup'); + final begin = DateTime.now().add(const Duration(hours: 1)); + final end = DateTime.now().add(const Duration(hours: 2)); + final todayExam = Exam( + '1232', + begin, + end, + 'SDIS', + rooms, + 'Recurso - Época Recurso (1ºS)', + 'feup', + ); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); }); test('When Exam was one hour ago', () async { - final DateTime end = DateTime.now().subtract(const Duration(hours: 1)); - final DateTime begin = DateTime.now().subtract(const Duration(hours: 2)); - final todayExam = Exam('1233', begin, end, 'SDIS', rooms, - 'Recurso - Época Recurso (1ºS)', 'feup'); + final end = DateTime.now().subtract(const Duration(hours: 1)); + final begin = DateTime.now().subtract(const Duration(hours: 2)); + final todayExam = Exam( + '1233', + begin, + end, + 'SDIS', + rooms, + 'Recurso - Época Recurso (1ºS)', + 'feup', + ); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.successful); - expect(provider.exams, []); + expect(provider.exams, []); }); test('When Exam is ocurring', () async { - final DateTime before = DateTime.now().subtract(const Duration(hours: 1)); - final DateTime after = DateTime.now().add(const Duration(hours: 1)); - final todayExam = Exam('1234', before, after, 'SDIS', rooms, - 'Recurso - Época Recurso (1ºS)', 'feup'); + final before = DateTime.now().subtract(const Duration(hours: 1)); + final after = DateTime.now().add(const Duration(hours: 1)); + final todayExam = Exam( + '1234', + before, + after, + 'SDIS', + rooms, + 'Recurso - Época Recurso (1ºS)', + 'feup', + ); when(parserExams.parseExams(any, any)) .thenAnswer((_) async => {todayExam}); await provider.fetchUserExams( - parserExams, userPersistentInfo, profile, session, userUcs); + parserExams, + userPersistentInfo, + profile, + session, + userUcs, + ); expect(provider.status, RequestStatus.successful); expect(provider.exams, [todayExam]); diff --git a/uni/test/unit/providers/lecture_provider_test.dart b/uni/test/unit/providers/lecture_provider_test.dart index e97e66be6..2f77e9acf 100644 --- a/uni/test/unit/providers/lecture_provider_test.dart +++ b/uni/test/unit/providers/lecture_provider_test.dart @@ -18,16 +18,33 @@ void main() { final fetcherMock = ScheduleFetcherMock(); final mockClient = MockClient(); final mockResponse = MockResponse(); - const Tuple2 userPersistentInfo = Tuple2('', ''); - final profile = Profile(); - profile.courses = [Course(id: 7474)]; + const userPersistentInfo = Tuple2('', ''); + final profile = Profile()..courses = [Course(id: 7474)]; final session = Session(username: '', cookies: '', faculties: ['feup']); - final day = DateTime(2021, 06, 01); + final day = DateTime(2021, 06); final lecture1 = Lecture.fromHtml( - 'SOPE', 'T', day, '10:00', 4, 'B315', 'JAS', 'MIEIC03', 484378); + 'SOPE', + 'T', + day, + '10:00', + 4, + 'B315', + 'JAS', + 'MIEIC03', + 484378, + ); final lecture2 = Lecture.fromHtml( - 'SDIS', 'T', day, '13:00', 4, 'B315', 'PMMS', 'MIEIC03', 484381); + 'SDIS', + 'T', + day, + '13:00', + 4, + 'B315', + 'PMMS', + 'MIEIC03', + 484381, + ); NetworkRouter.httpClient = mockClient; when(mockClient.get(any, headers: anyNamed('headers'))) @@ -44,8 +61,12 @@ void main() { when(fetcherMock.getLectures(any, any)) .thenAnswer((_) async => [lecture1, lecture2]); - await provider.fetchUserLectures(userPersistentInfo, session, profile, - fetcher: fetcherMock); + await provider.fetchUserLectures( + userPersistentInfo, + session, + profile, + fetcher: fetcherMock, + ); expect(provider.lectures, [lecture1, lecture2]); expect(provider.status, RequestStatus.successful); diff --git a/uni/test/unit/providers/mocks.dart b/uni/test/unit/providers/mocks.dart index fc9058b3e..edbe8cc80 100644 --- a/uni/test/unit/providers/mocks.dart +++ b/uni/test/unit/providers/mocks.dart @@ -1,7 +1,7 @@ +import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher.dart'; import 'package:uni/controller/parsers/parser_exams.dart'; -import 'package:http/http.dart' as http; class MockClient extends Mock implements http.Client {} 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 5cfbc1040..119e408c2 100644 --- a/uni/test/unit/view/Pages/exams_page_view_test.dart +++ b/uni/test/unit/view/Pages/exams_page_view_test.dart @@ -19,8 +19,7 @@ void main() { testWidgets('When given an empty list', (WidgetTester tester) async { const widget = ExamsPageView(); - final examProvider = ExamProvider(); - examProvider.setExams([]); + final examProvider = ExamProvider()..exams = []; final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; @@ -30,34 +29,54 @@ void main() { }); testWidgets('When given a single exam', (WidgetTester tester) async { - final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); - final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1230', firstExamBegin, firstExamEnd, - firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); + final firstExamBegin = DateTime.parse('$firstExamDate 09:00'); + final firstExamEnd = DateTime.parse('$firstExamDate 12:00'); + final firstExam = Exam( + '1230', + firstExamBegin, + firstExamEnd, + firstExamSubject, + ['B119', 'B107', 'B205'], + 'ER', + 'feup', + ); const widget = ExamsPageView(); - final examProvider = ExamProvider(); - examProvider.setExams([firstExam]); + final examProvider = ExamProvider()..exams = [firstExam]; final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); expect(find.byKey(Key(firstExam.toString())), findsOneWidget); - expect(find.byKey(Key('${firstExam.toString()}-exam')), findsOneWidget); + expect(find.byKey(Key('$firstExam-exam')), findsOneWidget); }); testWidgets('When given two exams from the same date', (WidgetTester tester) async { - final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); - final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1231', firstExamBegin, firstExamEnd, - firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); - final DateTime secondExamBegin = DateTime.parse('$firstExamDate 12:00'); - final DateTime secondExamEnd = DateTime.parse('$firstExamDate 15:00'); - final secondExam = Exam('1232', secondExamBegin, secondExamEnd, - secondExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); + final firstExamBegin = DateTime.parse('$firstExamDate 09:00'); + final firstExamEnd = DateTime.parse('$firstExamDate 12:00'); + final firstExam = Exam( + '1231', + firstExamBegin, + firstExamEnd, + firstExamSubject, + ['B119', 'B107', 'B205'], + 'ER', + 'feup', + ); + final secondExamBegin = DateTime.parse('$firstExamDate 12:00'); + final secondExamEnd = DateTime.parse('$firstExamDate 15:00'); + final secondExam = Exam( + '1232', + secondExamBegin, + secondExamEnd, + secondExamSubject, + ['B119', 'B107', 'B205'], + 'ER', + 'feup', + ); final examList = [ firstExam, @@ -66,29 +85,44 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider(); - examProvider.setExams(examList); + final examProvider = ExamProvider()..exams = examList; final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); - expect(find.byKey(Key(examList.map((ex) => ex.toString()).join())), - findsOneWidget); - expect(find.byKey(Key('${firstExam.toString()}-exam')), findsOneWidget); - expect(find.byKey(Key('${secondExam.toString()}-exam')), findsOneWidget); + expect( + find.byKey(Key(examList.map((ex) => ex.toString()).join())), + findsOneWidget, + ); + expect(find.byKey(Key('$firstExam-exam')), findsOneWidget); + expect(find.byKey(Key('$secondExam-exam')), findsOneWidget); }); testWidgets('When given two exams from different dates', (WidgetTester tester) async { - final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); - final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1233', firstExamBegin, firstExamEnd, - firstExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); - final DateTime secondExamBegin = DateTime.parse('$secondExamDate 12:00'); - final DateTime secondExamEnd = DateTime.parse('$secondExamDate 15:00'); - final secondExam = Exam('1234', secondExamBegin, secondExamEnd, - secondExamSubject, ['B119', 'B107', 'B205'], 'ER', 'feup'); + final firstExamBegin = DateTime.parse('$firstExamDate 09:00'); + final firstExamEnd = DateTime.parse('$firstExamDate 12:00'); + final firstExam = Exam( + '1233', + firstExamBegin, + firstExamEnd, + firstExamSubject, + ['B119', 'B107', 'B205'], + 'ER', + 'feup', + ); + final secondExamBegin = DateTime.parse('$secondExamDate 12:00'); + final secondExamEnd = DateTime.parse('$secondExamDate 15:00'); + final secondExam = Exam( + '1234', + secondExamBegin, + secondExamEnd, + secondExamSubject, + ['B119', 'B107', 'B205'], + 'ER', + 'feup', + ); final examList = [ firstExam, secondExam, @@ -96,43 +130,69 @@ void main() { const widget = ExamsPageView(); - final examProvider = ExamProvider(); - examProvider.setExams(examList); + final examProvider = ExamProvider()..exams = examList; final providers = [ChangeNotifierProvider(create: (_) => examProvider)]; await tester.pumpWidget(testableWidget(widget, providers: providers)); expect(find.byKey(Key(firstExam.toString())), findsOneWidget); expect(find.byKey(Key(secondExam.toString())), findsOneWidget); - expect(find.byKey(Key('${firstExam.toString()}-exam')), findsOneWidget); - expect(find.byKey(Key('${secondExam.toString()}-exam')), findsOneWidget); + expect(find.byKey(Key('$firstExam-exam')), findsOneWidget); + expect(find.byKey(Key('$secondExam-exam')), findsOneWidget); }); testWidgets('When given four exams from two different dates', (WidgetTester tester) async { - final List rooms = ['B119', 'B107', 'B205']; - final DateTime firstExamBegin = DateTime.parse('$firstExamDate 09:00'); - final DateTime firstExamEnd = DateTime.parse('$firstExamDate 12:00'); - final firstExam = Exam('1235', firstExamBegin, firstExamEnd, - firstExamSubject, rooms, 'ER', 'feup'); - final DateTime secondExamBegin = DateTime.parse('$firstExamDate 10:00'); - final DateTime secondExamEnd = DateTime.parse('$firstExamDate 12:00'); - final secondExam = Exam('1236', secondExamBegin, secondExamEnd, - firstExamSubject, rooms, 'ER', 'feup'); - final DateTime thirdExamBegin = DateTime.parse('$secondExamDate 12:00'); - final DateTime thirdExamEnd = DateTime.parse('$secondExamDate 15:00'); - final thirdExam = Exam('1237', thirdExamBegin, thirdExamEnd, - secondExamSubject, rooms, 'ER', 'feup'); - final DateTime fourthExamBegin = DateTime.parse('$secondExamDate 13:00'); - final DateTime fourthExamEnd = DateTime.parse('$secondExamDate 14:00'); - final fourthExam = Exam('1238', fourthExamBegin, fourthExamEnd, - secondExamSubject, rooms, 'ER', 'feup'); + final rooms = ['B119', 'B107', 'B205']; + final firstExamBegin = DateTime.parse('$firstExamDate 09:00'); + final firstExamEnd = DateTime.parse('$firstExamDate 12:00'); + final firstExam = Exam( + '1235', + firstExamBegin, + firstExamEnd, + firstExamSubject, + rooms, + 'ER', + 'feup', + ); + final secondExamBegin = DateTime.parse('$firstExamDate 10:00'); + final secondExamEnd = DateTime.parse('$firstExamDate 12:00'); + final secondExam = Exam( + '1236', + secondExamBegin, + secondExamEnd, + firstExamSubject, + rooms, + 'ER', + 'feup', + ); + final thirdExamBegin = DateTime.parse('$secondExamDate 12:00'); + final thirdExamEnd = DateTime.parse('$secondExamDate 15:00'); + final thirdExam = Exam( + '1237', + thirdExamBegin, + thirdExamEnd, + secondExamSubject, + rooms, + 'ER', + 'feup', + ); + final fourthExamBegin = DateTime.parse('$secondExamDate 13:00'); + final fourthExamEnd = DateTime.parse('$secondExamDate 14:00'); + final fourthExam = Exam( + '1238', + fourthExamBegin, + fourthExamEnd, + secondExamSubject, + rooms, + 'ER', + 'feup', + ); final examList = [firstExam, secondExam, thirdExam, fourthExam]; const widget = ExamsPageView(); - final examProvider = ExamProvider(); - examProvider.setExams(examList); + final examProvider = ExamProvider()..exams = examList; final firstDayKey = [firstExam, secondExam].map((ex) => ex.toString()).join(); @@ -144,10 +204,10 @@ void main() { await tester.pumpWidget(testableWidget(widget, providers: providers)); expect(find.byKey(Key(firstDayKey)), findsOneWidget); expect(find.byKey(Key(secondDayKey)), findsOneWidget); - expect(find.byKey(Key('${firstExam.toString()}-exam')), findsOneWidget); - expect(find.byKey(Key('${secondExam.toString()}-exam')), findsOneWidget); - expect(find.byKey(Key('${thirdExam.toString()}-exam')), findsOneWidget); - expect(find.byKey(Key('${fourthExam.toString()}-exam')), findsOneWidget); + expect(find.byKey(Key('$firstExam-exam')), findsOneWidget); + expect(find.byKey(Key('$secondExam-exam')), findsOneWidget); + expect(find.byKey(Key('$thirdExam-exam')), findsOneWidget); + expect(find.byKey(Key('$fourthExam-exam')), 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 e8eea58d1..f6869893d 100644 --- a/uni/test/unit/view/Pages/schedule_page_view_test.dart +++ b/uni/test/unit/view/Pages/schedule_page_view_test.dart @@ -18,19 +18,73 @@ void main() { final day4 = DateTime(2021, 06, 11); final lecture1 = Lecture.fromHtml( - 'SOPE', 'T', day0, '10:00', blocks, 'B315', 'JAS', classNumber, 484378); - final lecture2 = Lecture.fromHtml('SDIS', 'T', day0, '13:00', blocks, - 'B315', 'PMMS', classNumber, 484381); - final lecture3 = Lecture.fromHtml('AMAT', 'T', day1, '12:00', blocks, - 'B315', 'PMMS', classNumber, 484362); + 'SOPE', + 'T', + day0, + '10:00', + blocks, + 'B315', + 'JAS', + classNumber, + 484378, + ); + final lecture2 = Lecture.fromHtml( + 'SDIS', + 'T', + day0, + '13:00', + blocks, + 'B315', + 'PMMS', + classNumber, + 484381, + ); + final lecture3 = Lecture.fromHtml( + 'AMAT', + 'T', + day1, + '12:00', + blocks, + 'B315', + 'PMMS', + classNumber, + 484362, + ); final lecture4 = Lecture.fromHtml( - 'PROG', 'T', day2, '10:00', blocks, 'B315', 'JAS', classNumber, 484422); + 'PROG', + 'T', + day2, + '10:00', + blocks, + 'B315', + 'JAS', + classNumber, + 484422, + ); final lecture5 = Lecture.fromHtml( - 'PPIN', 'T', day3, '14:00', blocks, 'B315', 'SSN', classNumber, 47775); + 'PPIN', + 'T', + day3, + '14:00', + blocks, + 'B315', + 'SSN', + classNumber, + 47775, + ); final lecture6 = Lecture.fromHtml( - 'SDIS', 'T', day4, '15:00', blocks, 'B315', 'PMMS', classNumber, 12345); - - final List daysOfTheWeek = [ + 'SDIS', + 'T', + day4, + '15:00', + blocks, + 'B315', + 'PMMS', + classNumber, + 12345, + ); + + final daysOfTheWeek = [ 'Segunda-feira', 'Terça-feira', 'Quarta-feira', @@ -41,101 +95,122 @@ void main() { testWidgets('When given one lecture on a single day', (WidgetTester tester) async { final widget = SchedulePageView( - lectures: [lecture1], scheduleStatus: RequestStatus.successful); + lectures: [lecture1], + scheduleStatus: RequestStatus.successful, + ); await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); - final SchedulePageViewState myWidgetState = - tester.state(find.byType(SchedulePageView)); + final myWidgetState = + tester.state(find.byType(SchedulePageView)) as SchedulePageViewState; myWidgetState.tabController!.animateTo(0); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-0')), - matching: find.byType(ScheduleSlot)), - findsOneWidget); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-0')), + matching: find.byType(ScheduleSlot), + ), + findsOneWidget, + ); }); testWidgets('When given two lectures on a single day', (WidgetTester tester) async { final widget = SchedulePageView( - lectures: [lecture1, lecture2], - scheduleStatus: RequestStatus.successful); + lectures: [lecture1, lecture2], + scheduleStatus: RequestStatus.successful, + ); await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); - final SchedulePageViewState myWidgetState = - tester.state(find.byType(SchedulePageView)); + final myWidgetState = + tester.state(find.byType(SchedulePageView)) as SchedulePageViewState; myWidgetState.tabController!.animateTo(0); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-0')), - matching: find.byType(ScheduleSlot)), - findsNWidgets(2)); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-0')), + matching: find.byType(ScheduleSlot), + ), + findsNWidgets(2), + ); }); testWidgets('When given lectures on different days', (WidgetTester tester) async { final widget = DefaultTabController( - length: daysOfTheWeek.length, - child: SchedulePageView(lectures: [ + length: daysOfTheWeek.length, + child: SchedulePageView( + lectures: [ lecture1, lecture2, lecture3, lecture4, lecture5, lecture6 - ], scheduleStatus: RequestStatus.successful)); + ], + scheduleStatus: RequestStatus.successful, + ), + ); await tester.pumpWidget(testableWidget(widget, providers: [])); await tester.pumpAndSettle(); - final SchedulePageViewState myWidgetState = - tester.state(find.byType(SchedulePageView)); + final myWidgetState = + tester.state(find.byType(SchedulePageView)) as SchedulePageViewState; myWidgetState.tabController!.animateTo(0); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-0')), - matching: find.byType(ScheduleSlot)), - findsNWidgets(2)); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-0')), + matching: find.byType(ScheduleSlot), + ), + findsNWidgets(2), + ); await tester.tap(find.byKey(const Key('schedule-page-tab-1'))); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-1')), - matching: find.byType(ScheduleSlot)), - findsOneWidget); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-1')), + matching: find.byType(ScheduleSlot), + ), + findsOneWidget, + ); await tester.tap(find.byKey(const Key('schedule-page-tab-2'))); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-2')), - matching: find.byType(ScheduleSlot)), - findsOneWidget); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-2')), + matching: find.byType(ScheduleSlot), + ), + findsOneWidget, + ); await tester.tap(find.byKey(const Key('schedule-page-tab-3'))); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-3')), - matching: find.byType(ScheduleSlot)), - findsOneWidget); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-3')), + matching: find.byType(ScheduleSlot), + ), + findsOneWidget, + ); await tester.tap(find.byKey(const Key('schedule-page-tab-4'))); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byKey(const Key('schedule-page-day-column-4')), - matching: find.byType(ScheduleSlot)), - findsOneWidget); + find.descendant( + of: find.byKey(const Key('schedule-page-day-column-4')), + matching: find.byType(ScheduleSlot), + ), + findsOneWidget, + ); }); }); } diff --git a/uni/test/unit/view/Widgets/exam_row_test.dart b/uni/test/unit/view/Widgets/exam_row_test.dart index 62459a32f..47ea0bac1 100644 --- a/uni/test/unit/view/Widgets/exam_row_test.dart +++ b/uni/test/unit/view/Widgets/exam_row_test.dart @@ -11,16 +11,24 @@ import '../../../test_widget.dart'; void main() { group('Exam Row', () { const subject = 'SOPE'; - final DateTime begin = DateTime( - DateTime.now().year, DateTime.now().month, DateTime.now().day, 10, 0); - final DateTime end = DateTime( - DateTime.now().year, DateTime.now().month, DateTime.now().day, 12, 0); - final String beginTime = DateFormat('HH:mm').format(begin); - final String endTime = DateFormat('HH:mm').format(end); + final begin = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + 10, + ); + final end = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + 12, + ); + final beginTime = DateFormat('HH:mm').format(begin); + final endTime = DateFormat('HH:mm').format(end); testWidgets('When given a single room', (WidgetTester tester) async { final rooms = ['B315']; - final Exam exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); + final exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); final widget = ExamRow(exam: exam, teacher: '', mainPage: true); final providers = [ @@ -30,14 +38,17 @@ void main() { final roomsKey = '$subject-$rooms-$beginTime-$endTime'; expect( - find.descendant( - of: find.byKey(Key(roomsKey)), matching: find.byType(Text)), - findsOneWidget); + find.descendant( + of: find.byKey(Key(roomsKey)), + matching: find.byType(Text), + ), + findsOneWidget, + ); }); testWidgets('When multiple rooms', (WidgetTester tester) async { final rooms = ['B315', 'B316', 'B330']; - final Exam exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); + final exam = Exam('1230', begin, end, subject, rooms, '', 'feup'); final widget = ExamRow(exam: exam, teacher: '', mainPage: true); final providers = [ @@ -48,9 +59,12 @@ void main() { final roomsKey = '$subject-$rooms-$beginTime-$endTime'; expect( - find.descendant( - of: find.byKey(Key(roomsKey)), matching: find.byType(Text)), - findsNWidgets(3)); + find.descendant( + of: find.byKey(Key(roomsKey)), + matching: find.byType(Text), + ), + findsNWidgets(3), + ); }); }); } diff --git a/uni/test/unit/view/Widgets/schedule_slot_test.dart b/uni/test/unit/view/Widgets/schedule_slot_test.dart index 195b386ac..2a85c7c3a 100644 --- a/uni/test/unit/view/Widgets/schedule_slot_test.dart +++ b/uni/test/unit/view/Widgets/schedule_slot_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; - import 'package:uni/view/schedule/widgets/schedule_slot.dart'; import '../../../test_widget.dart'; @@ -9,10 +8,10 @@ import '../../../test_widget.dart'; void main() { group('Schedule Slot', () { const subject = 'SOPE'; - final DateTime begin = DateTime(2021, 06, 01, 10, 00); - final beginText = DateFormat("HH:mm").format(begin); - final DateTime end = DateTime(2021, 06, 01, 12, 00); - final endText = DateFormat("HH:mm").format(end); + final begin = DateTime(2021, 06, 01, 10); + final beginText = DateFormat('HH:mm').format(begin); + final end = DateTime(2021, 06, 01, 12); + final endText = DateFormat('HH:mm').format(end); const rooms = 'B315'; const typeClass = 'T'; const teacher = 'JAS'; @@ -36,25 +35,41 @@ void main() { }); } -void testScheduleSlot(String subject, String begin, String end, String rooms, - String typeClass, String teacher) { +void testScheduleSlot( + String subject, + String begin, + String end, + String rooms, + String typeClass, + String teacher, +) { final scheduleSlotTimeKey = 'schedule-slot-time-$begin-$end'; expect( - find.descendant( - of: find.byKey(Key(scheduleSlotTimeKey)), matching: find.text(begin)), - findsOneWidget); + find.descendant( + of: find.byKey(Key(scheduleSlotTimeKey)), + matching: find.text(begin), + ), + findsOneWidget, + ); expect( - find.descendant( - of: find.byKey(Key(scheduleSlotTimeKey)), matching: find.text(end)), - findsOneWidget); + find.descendant( + of: find.byKey(Key(scheduleSlotTimeKey)), + matching: find.text(end), + ), + findsOneWidget, + ); expect( - find.descendant( - of: find.byKey(Key(scheduleSlotTimeKey)), - matching: find.text(subject)), - findsOneWidget); + find.descendant( + of: find.byKey(Key(scheduleSlotTimeKey)), + matching: find.text(subject), + ), + findsOneWidget, + ); expect( - find.descendant( - of: find.byKey(Key(scheduleSlotTimeKey)), - matching: find.text(' ($typeClass)')), - findsOneWidget); + find.descendant( + of: find.byKey(Key(scheduleSlotTimeKey)), + matching: find.text(' ($typeClass)'), + ), + findsOneWidget, + ); }