diff --git a/packages/uni_app/lib/controller/fetchers/exam_fetcher.dart b/packages/uni_app/lib/controller/fetchers/exam_fetcher.dart index 8d79b3ece..036393455 100644 --- a/packages/uni_app/lib/controller/fetchers/exam_fetcher.dart +++ b/packages/uni_app/lib/controller/fetchers/exam_fetcher.dart @@ -47,7 +47,7 @@ class ExamFetcher implements SessionDependantFetcher { ) && courseExam.examType != 'EE' && courseExam.examType != 'EAE' && - courseExam.subject == uc.abbreviation && + courseExam.subjectAcronym == uc.abbreviation && uc.enrollmentIsValid() && !courseExam.hasEnded()) { exams.add(courseExam); diff --git a/packages/uni_app/lib/controller/local_storage/database/app_exams_database.dart b/packages/uni_app/lib/controller/local_storage/database/app_exams_database.dart index b628ddcf0..c087a05e3 100644 --- a/packages/uni_app/lib/controller/local_storage/database/app_exams_database.dart +++ b/packages/uni_app/lib/controller/local_storage/database/app_exams_database.dart @@ -10,10 +10,10 @@ import 'package:uni/model/entities/exam.dart'; /// See the [Exam] class to see what data is stored in this database. class AppExamsDatabase extends AppDatabase> { AppExamsDatabase() - : super('exams.db', [_createScript], onUpgrade: migrate, version: 7); + : super('exams.db', [_createScript], onUpgrade: migrate, version: 8); static const _createScript = ''' -CREATE TABLE exams(id TEXT, subject TEXT, start TEXT, finish TEXT, +CREATE TABLE exams(id TEXT, subjectAcronym TEXT, subject TEXT, start TEXT, finish TEXT, rooms TEXT, examType TEXT, faculty TEXT, PRIMARY KEY (id,faculty)) '''; /// Returns a list containing all of the exams stored in this database. @@ -24,6 +24,7 @@ CREATE TABLE exams(id TEXT, subject TEXT, start TEXT, finish TEXT, return List.generate(maps.length, (i) { return Exam.secConstructor( maps[i]['id'] as String, + maps[i]['subjectAcronym'] as String, maps[i]['subject'] as String, DateTime.parse(maps[i]['start'] as String), DateTime.parse(maps[i]['finish'] as String), diff --git a/packages/uni_app/lib/controller/parsers/parser_exams.dart b/packages/uni_app/lib/controller/parsers/parser_exams.dart index 8069410a6..1506e72f9 100644 --- a/packages/uni_app/lib/controller/parsers/parser_exams.dart +++ b/packages/uni_app/lib/controller/parsers/parser_exams.dart @@ -27,6 +27,7 @@ class ParserExams { final dates = []; final examTypes = []; var rooms = []; + String? subjectAcronym; String? subject; var id = '0'; var days = 0; @@ -46,7 +47,8 @@ class ParserExams { if (exams.querySelector('td.exame') != null) { exams.querySelectorAll('td.exame').forEach((examsDay) { if (examsDay.querySelector('a') != null) { - subject = examsDay.querySelector('a')!.text; + subjectAcronym = examsDay.querySelector('a')!.text; + subject = examsDay.querySelector('a')!.attributes['title']; id = Uri.parse(examsDay.querySelector('a')!.attributes['href']!) .queryParameters['p_exa_id']!; } @@ -56,6 +58,7 @@ class ParserExams { .text .split(',') .map((e) => e.trim()) + .where((e) => e.isNotEmpty) .toList(); } final DateTime begin; @@ -73,6 +76,7 @@ class ParserExams { id, begin, end, + subjectAcronym ?? '', subject ?? '', rooms, examTypes[tableNum], diff --git a/packages/uni_app/lib/generated/controller/parsers/schedule/new_api/models/response_lecture_unit.g.dart b/packages/uni_app/lib/generated/controller/parsers/schedule/new_api/models/response_lecture_unit.g.dart index 1d1ba95b8..6844ccc8a 100644 --- a/packages/uni_app/lib/generated/controller/parsers/schedule/new_api/models/response_lecture_unit.g.dart +++ b/packages/uni_app/lib/generated/controller/parsers/schedule/new_api/models/response_lecture_unit.g.dart @@ -9,7 +9,7 @@ part of '../../../../../../controller/parsers/schedule/new_api/models/response_l ResponseLectureUnit _$ResponseLectureUnitFromJson(Map json) => ResponseLectureUnit( json['acronym'] as String, - json['sigarra_id'] as int, + (json['sigarra_id'] as num).toInt(), ); Map _$ResponseLectureUnitToJson( diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart index 24826a3e3..5968962c5 100644 --- a/packages/uni_app/lib/generated/intl/messages_en.dart +++ b/packages/uni_app/lib/generated/intl/messages_en.dart @@ -102,6 +102,7 @@ class MessageLookup extends MessageLookupByLibrary { "Floor -1 of building B | AEFEUP building"), "course_class": MessageLookupByLibrary.simpleMessage("Classes"), "course_info": MessageLookupByLibrary.simpleMessage("Info"), + "courses": MessageLookupByLibrary.simpleMessage("Courses"), "current_state": MessageLookupByLibrary.simpleMessage("Current state: "), "current_year": @@ -122,6 +123,7 @@ class MessageLookup extends MessageLookupByLibrary { "empty_text": MessageLookupByLibrary.simpleMessage("Please fill in this field"), "evaluation": MessageLookupByLibrary.simpleMessage("Evaluation"), + "exams": MessageLookupByLibrary.simpleMessage("Exams"), "exams_filter": MessageLookupByLibrary.simpleMessage("Exams Filter Settings"), "exit_confirm": @@ -157,6 +159,7 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Language"), "last_refresh_time": m0, "last_timestamp": m1, + "lectures": MessageLookupByLibrary.simpleMessage("Lectures"), "library_occupation": MessageLookupByLibrary.simpleMessage("Library Occupation"), "load_error": MessageLookupByLibrary.simpleMessage( diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart index 502e9f9ec..f36592457 100644 --- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart +++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart @@ -102,6 +102,7 @@ class MessageLookup extends MessageLookupByLibrary { "Piso -1 do edifício B | Edifício da AEFEUP"), "course_class": MessageLookupByLibrary.simpleMessage("Turmas"), "course_info": MessageLookupByLibrary.simpleMessage("Ficha"), + "courses": MessageLookupByLibrary.simpleMessage("Cursos"), "current_state": MessageLookupByLibrary.simpleMessage("Estado atual: "), "current_year": MessageLookupByLibrary.simpleMessage("Ano curricular atual: "), @@ -121,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary { "empty_text": MessageLookupByLibrary.simpleMessage( "Por favor preenche este campo"), "evaluation": MessageLookupByLibrary.simpleMessage("Avaliação"), + "exams": MessageLookupByLibrary.simpleMessage("Exames"), "exams_filter": MessageLookupByLibrary.simpleMessage("Definições Filtro de Exames"), "exit_confirm": MessageLookupByLibrary.simpleMessage( @@ -156,6 +158,7 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Idioma"), "last_refresh_time": m0, "last_timestamp": m1, + "lectures": MessageLookupByLibrary.simpleMessage("Aulas"), "library_occupation": MessageLookupByLibrary.simpleMessage("Ocupação da Biblioteca"), "load_error": MessageLookupByLibrary.simpleMessage( diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart index 4201d66c1..7907355bf 100644 --- a/packages/uni_app/lib/generated/l10n.dart +++ b/packages/uni_app/lib/generated/l10n.dart @@ -1697,6 +1697,36 @@ class S { args: [], ); } + + /// `Lectures` + String get lectures { + return Intl.message( + 'Lectures', + name: 'lectures', + desc: '', + args: [], + ); + } + + /// `Exams` + String get exams { + return Intl.message( + 'Exams', + name: 'exams', + desc: '', + args: [], + ); + } + + /// `Courses` + String get courses { + return Intl.message( + 'Courses', + name: 'courses', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/packages/uni_app/lib/generated/model/entities/course.g.dart b/packages/uni_app/lib/generated/model/entities/course.g.dart index d51524b13..a2b3fd763 100644 --- a/packages/uni_app/lib/generated/model/entities/course.g.dart +++ b/packages/uni_app/lib/generated/model/entities/course.g.dart @@ -8,7 +8,7 @@ part of '../../../model/entities/course.dart'; Map _$CourseToJson(Course instance) => { 'cur_id': instance.id, - 'fest_id ': instance.festId, + 'fest_id': instance.festId, 'cur_nome': instance.name, 'abbreviation': instance.abbreviation, 'ano_curricular': instance.currYear, diff --git a/packages/uni_app/lib/generated/model/entities/course_units/course_unit.g.dart b/packages/uni_app/lib/generated/model/entities/course_units/course_unit.g.dart index dba0cfd22..17c5b0503 100644 --- a/packages/uni_app/lib/generated/model/entities/course_units/course_unit.g.dart +++ b/packages/uni_app/lib/generated/model/entities/course_units/course_unit.g.dart @@ -9,10 +9,10 @@ part of '../../../../model/entities/course_units/course_unit.dart'; CourseUnit _$CourseUnitFromJson(Map json) => CourseUnit( abbreviation: json['ucurr_sigla'] as String, name: json['ucurr_nome'] as String, - occurrId: json['ocorr_id'] as int?, - id: json['ucurr_id'] as int? ?? 0, + occurrId: (json['ocorr_id'] as num?)?.toInt(), + id: (json['ucurr_id'] as num?)?.toInt() ?? 0, code: json['ucurr_codigo'] as String? ?? '', - curricularYear: json['ano'] as int?, + curricularYear: (json['ano'] as num?)?.toInt(), semesterCode: json['per_codigo'] as String?, semesterName: json['per_nome'] as String?, type: json['tipo'] as String?, diff --git a/packages/uni_app/lib/generated/model/entities/exam.g.dart b/packages/uni_app/lib/generated/model/entities/exam.g.dart index 837cd7646..096f779eb 100644 --- a/packages/uni_app/lib/generated/model/entities/exam.g.dart +++ b/packages/uni_app/lib/generated/model/entities/exam.g.dart @@ -10,6 +10,7 @@ Exam _$ExamFromJson(Map json) => Exam( json['id'] as String, const DateTimeConverter().fromJson(json['start'] as String), const DateTimeConverter().fromJson(json['finish'] as String), + json['subjectAcronym'] as String, json['subject'] as String, (json['rooms'] as List).map((e) => e as String).toList(), json['examType'] as String, @@ -20,6 +21,7 @@ Map _$ExamToJson(Exam instance) => { 'start': const DateTimeConverter().toJson(instance.start), 'finish': const DateTimeConverter().toJson(instance.finish), 'id': instance.id, + 'subjectAcronym': instance.subjectAcronym, 'subject': instance.subject, 'rooms': instance.rooms, 'examType': instance.examType, diff --git a/packages/uni_app/lib/generated/model/entities/lecture.g.dart b/packages/uni_app/lib/generated/model/entities/lecture.g.dart index 66a4c44aa..fcc77c8ad 100644 --- a/packages/uni_app/lib/generated/model/entities/lecture.g.dart +++ b/packages/uni_app/lib/generated/model/entities/lecture.g.dart @@ -14,7 +14,7 @@ Lecture _$LectureFromJson(Map json) => Lecture( json['room'] as String, json['teacher'] as String, json['classNumber'] as String, - json['occurrId'] as int, + (json['occurrId'] as num).toInt(), ); Map _$LectureToJson(Lecture instance) => { diff --git a/packages/uni_app/lib/generated/model/entities/library_occupation.g.dart b/packages/uni_app/lib/generated/model/entities/library_occupation.g.dart index fe8e7e4bb..4cd1c67ee 100644 --- a/packages/uni_app/lib/generated/model/entities/library_occupation.g.dart +++ b/packages/uni_app/lib/generated/model/entities/library_occupation.g.dart @@ -8,8 +8,8 @@ part of '../../../model/entities/library_occupation.dart'; LibraryOccupation _$LibraryOccupationFromJson(Map json) => LibraryOccupation( - json['occupation'] as int, - json['capacity'] as int, + (json['occupation'] as num).toInt(), + (json['capacity'] as num).toInt(), )..floors = (json['floors'] as List) .map((e) => FloorOccupation.fromJson(e as Map)) .toList(); @@ -23,9 +23,9 @@ Map _$LibraryOccupationToJson(LibraryOccupation instance) => FloorOccupation _$FloorOccupationFromJson(Map json) => FloorOccupation( - json['number'] as int, - json['occupation'] as int, - json['capacity'] as int, + (json['number'] as num).toInt(), + (json['occupation'] as num).toInt(), + (json['capacity'] as num).toInt(), ); Map _$FloorOccupationToJson(FloorOccupation instance) => diff --git a/packages/uni_app/lib/generated/model/entities/location_group.g.dart b/packages/uni_app/lib/generated/model/entities/location_group.g.dart index 4ca96e363..beb8245d1 100644 --- a/packages/uni_app/lib/generated/model/entities/location_group.g.dart +++ b/packages/uni_app/lib/generated/model/entities/location_group.g.dart @@ -10,7 +10,7 @@ LocationGroup _$LocationGroupFromJson(Map json) => LocationGroup( LatLng.fromJson(json['latlng'] as Map), isFloorless: json['isFloorless'] as bool? ?? false, - id: json['id'] as int?, + id: (json['id'] as num?)?.toInt(), ); Map _$LocationGroupToJson(LocationGroup instance) => diff --git a/packages/uni_app/lib/generated/model/entities/reference.g.dart b/packages/uni_app/lib/generated/model/entities/reference.g.dart index 53b2a3150..6778cd83b 100644 --- a/packages/uni_app/lib/generated/model/entities/reference.g.dart +++ b/packages/uni_app/lib/generated/model/entities/reference.g.dart @@ -9,8 +9,8 @@ part of '../../../model/entities/reference.dart'; Reference _$ReferenceFromJson(Map json) => Reference( json['description'] as String, const DateTimeConverter().fromJson(json['limitDate'] as String), - json['entity'] as int, - json['reference'] as int, + (json['entity'] as num).toInt(), + (json['reference'] as num).toInt(), (json['amount'] as num).toDouble(), ); diff --git a/packages/uni_app/lib/generated/model/entities/restaurant.g.dart b/packages/uni_app/lib/generated/model/entities/restaurant.g.dart index 5471eb4f0..bd3f3be3b 100644 --- a/packages/uni_app/lib/generated/model/entities/restaurant.g.dart +++ b/packages/uni_app/lib/generated/model/entities/restaurant.g.dart @@ -7,7 +7,7 @@ part of '../../../model/entities/restaurant.dart'; // ************************************************************************** Restaurant _$RestaurantFromJson(Map json) => Restaurant( - json['id'] as int?, + (json['id'] as num?)?.toInt(), json['name'] as String, json['ref'] as String, meals: (json['meals'] as List) diff --git a/packages/uni_app/lib/generated/model/entities/trip.g.dart b/packages/uni_app/lib/generated/model/entities/trip.g.dart index 973f9d436..bdf83bd20 100644 --- a/packages/uni_app/lib/generated/model/entities/trip.g.dart +++ b/packages/uni_app/lib/generated/model/entities/trip.g.dart @@ -9,7 +9,7 @@ part of '../../../model/entities/trip.dart'; Trip _$TripFromJson(Map json) => Trip( line: json['line'] as String, destination: json['destination'] as String, - timeRemaining: json['timeRemaining'] as int, + timeRemaining: (json['timeRemaining'] as num).toInt(), ); Map _$TripToJson(Trip instance) => { diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb index 5e3eb22a6..e98444d96 100644 --- a/packages/uni_app/lib/l10n/intl_en.arb +++ b/packages/uni_app/lib/l10n/intl_en.arb @@ -333,5 +333,11 @@ "wrong_credentials_exception": "Invalid credentials", "@wrong_credentials_exception": {}, "internet_status_exception": "Check your internet connection", - "@internet_status_exception": {} + "@internet_status_exception": {}, + "lectures": "Lectures", + "@lectures": {}, + "exams": "Exams", + "@exams": {}, + "courses": "Courses", + "@courses": {} } \ No newline at end of file diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb index 4212fe69c..9835f0071 100644 --- a/packages/uni_app/lib/l10n/intl_pt_PT.arb +++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb @@ -333,5 +333,11 @@ "wrong_credentials_exception": "Credenciais inválidas", "@wrong_credentials_exception": {}, "internet_status_exception": "Verifique sua conexão com a internet", - "@internet_status_exception": {} + "@internet_status_exception": {}, + "lectures": "Aulas", + "@lectures": {}, + "exams": "Exames", + "@exams": {}, + "courses": "Cursos", + "@courses": {} } \ No newline at end of file diff --git a/packages/uni_app/lib/main.dart b/packages/uni_app/lib/main.dart index 410b31835..4d9c6cc44 100644 --- a/packages/uni_app/lib/main.dart +++ b/packages/uni_app/lib/main.dart @@ -50,6 +50,7 @@ import 'package:uni/view/settings/settings.dart'; import 'package:uni/view/theme.dart'; import 'package:uni/view/theme_notifier.dart'; import 'package:uni/view/transports/transports.dart'; +import 'package:uni_ui/theme.dart'; import 'package:upgrader/upgrader.dart'; import 'package:workmanager/workmanager.dart'; @@ -224,7 +225,7 @@ class ApplicationState extends State { child: MaterialApp( title: 'uni', navigatorKey: Application.navigatorKey, - theme: applicationLightTheme, + theme: lightTheme, darkTheme: applicationDarkTheme, themeMode: themeNotifier.getTheme(), locale: localeNotifier.getLocale().localeCode, diff --git a/packages/uni_app/lib/model/entities/exam.dart b/packages/uni_app/lib/model/entities/exam.dart index 280661e2d..9769ebc49 100644 --- a/packages/uni_app/lib/model/entities/exam.dart +++ b/packages/uni_app/lib/model/entities/exam.dart @@ -21,6 +21,7 @@ class Exam { this.id, this.start, this.finish, + this.subjectAcronym, this.subject, this.rooms, this.examType, @@ -31,6 +32,7 @@ class Exam { Exam.secConstructor( this.id, + this.subjectAcronym, this.subject, this.start, this.finish, @@ -42,6 +44,7 @@ class Exam { final DateTime start; final DateTime finish; final String id; + final String subjectAcronym; final String subject; final List rooms; final String examType; @@ -67,6 +70,7 @@ class Exam { .WEEKDAYS[start.weekday % 7]; } + // TODO(thePeras): Remove this method and use {start.month} in the toString. Tests will fail and need to be updated. String month(AppLocale locale) { return DateFormat.EEEE(locale.localeCode.languageCode) .dateSymbols @@ -81,7 +85,7 @@ class Exam { @override String toString() { - return '''$id - $subject - ${start.year} - $month - ${start.day} - $startTime-$finishTime - $examType - $rooms - $weekDay'''; + return '''$id - $subjectAcronym - ${start.year} - $month - ${start.day} - $startTime-$finishTime - $examType - $rooms - $weekDay'''; } /// Prints the data in this exam to the [Logger] with an INFO level. @@ -92,7 +96,7 @@ class Exam { @override bool operator ==(Object other) => identical(this, other) || - other is Exam && id == other.id && subject == other.subject; + other is Exam && id == other.id && subjectAcronym == other.subjectAcronym; @override int get hashCode => id.hashCode; diff --git a/packages/uni_app/lib/utils/date_time_formatter.dart b/packages/uni_app/lib/utils/date_time_formatter.dart index 840b4cc91..aaafbde06 100644 --- a/packages/uni_app/lib/utils/date_time_formatter.dart +++ b/packages/uni_app/lib/utils/date_time_formatter.dart @@ -8,10 +8,17 @@ extension DateTimeExtensions on DateTime { .WEEKDAYS[weekday % 7]; } - String month(AppLocale locale) { + String fullMonth(AppLocale locale) { return DateFormat.EEEE(locale.localeCode.languageCode) .dateSymbols - .MONTHS[this.month - 1]; + .MONTHS[month - 1]; + } + + String shortMonth(AppLocale locale) { + return DateFormat.EEEE(locale.localeCode.languageCode) + .dateSymbols + .SHORTMONTHS[month - 1] + .replaceAll('.', ''); } String formattedDate(AppLocale locale) { diff --git a/packages/uni_app/lib/utils/string_formatter.dart b/packages/uni_app/lib/utils/string_formatter.dart new file mode 100644 index 000000000..bfc20263f --- /dev/null +++ b/packages/uni_app/lib/utils/string_formatter.dart @@ -0,0 +1,5 @@ +extension StringExtension on String { + String capitalize() { + return '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; + } +} diff --git a/packages/uni_app/lib/view/academic_path/academic_path.dart b/packages/uni_app/lib/view/academic_path/academic_path.dart index 243b6aa08..bfac776d1 100644 --- a/packages/uni_app/lib/view/academic_path/academic_path.dart +++ b/packages/uni_app/lib/view/academic_path/academic_path.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/utils/navigation_items.dart'; -import 'package:uni/view/academic_path/widgets/course_units_card.dart'; -import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/academic_path/exam_page.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; -import 'package:uni/view/home/widgets/exam_card.dart'; -import 'package:uni/view/home/widgets/schedule_card.dart'; +import 'package:uni_ui/icons.dart'; +import 'package:uni_ui/tabs/tab_icon.dart'; class AcademicPathPageView extends StatefulWidget { const AcademicPathPageView({super.key}); @@ -14,29 +13,60 @@ class AcademicPathPageView extends StatefulWidget { State createState() => AcademicPathPageViewState(); } -class AcademicPathPageViewState extends GeneralPageViewState { - List academicPathCards = [ - ScheduleCard(), - ExamCard(), - CourseUnitsCard(), - // Add more cards if needed - ]; - +class AcademicPathPageViewState extends GeneralPageViewState + with SingleTickerProviderStateMixin { @override String? getTitle() => S.of(context).nav_title(NavigationItem.navAcademicPath.route); + late TabController tabController; + + @override + void initState() { + super.initState(); + tabController = TabController(vsync: this, length: 3); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + @override + Widget? getHeader(BuildContext context) { + return TabBar( + controller: tabController, + dividerHeight: 1, + tabs: [ + TabIcon( + icon: UniIcons.lecture, + text: S.of(context).lectures, + ), + TabIcon(icon: UniIcons.exam, text: S.of(context).exams), + TabIcon(icon: UniIcons.course, text: S.of(context).courses), + ], + ); + } + @override Widget getBody(BuildContext context) { - return ListView( - children: academicPathCards, + return TabBarView( + controller: tabController, + children: const [ + Center( + child: Text('To be implemented'), + ), + ExamsPage(), + Center( + child: Text('To be implemented'), + ), + ], ); } @override Future onRefresh(BuildContext context) async { - for (final card in academicPathCards) { - card.onRefresh(context); - } + // TODO: implement onRefresh } } diff --git a/packages/uni_app/lib/view/academic_path/exam_page.dart b/packages/uni_app/lib/view/academic_path/exam_page.dart new file mode 100644 index 000000000..221ba299d --- /dev/null +++ b/packages/uni_app/lib/view/academic_path/exam_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/preferences_controller.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/exam.dart'; +import 'package:uni/model/providers/lazy/exam_provider.dart'; +import 'package:uni/utils/date_time_formatter.dart'; +import 'package:uni/utils/string_formatter.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/locale_notifier.dart'; +import 'package:uni_ui/cards/exam_card.dart'; +import 'package:uni_ui/cards/timeline_card.dart'; + +class ExamsPage extends StatefulWidget { + const ExamsPage({super.key}); + + @override + State createState() => _ExamsPageState(); +} + +class _ExamsPageState extends State { + List hiddenExams = PreferencesController.getHiddenExams(); + Map filteredExamTypes = + PreferencesController.getFilteredExams(); + + @override + Widget build(BuildContext context) { + /* + If we want to filters exams again + filteredExamTypes[Exam.getExamTypeLong(exam.examType)] ?? + */ + + return LazyConsumer>( + builder: (context, exams) => ListView( + children: _examsByMonth(exams) + .entries + .map( + (entry) => Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateTime( + int.parse(entry.key.split('-')[0]), + int.parse(entry.key.split('-')[1]), + ) + .fullMonth( + Provider.of(context).getLocale(), + ) + .capitalize(), + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: entry.value.length, + prototypeItem: const TimelineItem( + title: '1', + subtitle: 'Jan', + card: ExamCard( + name: 'Computer Laboratory', + acronym: 'LCOM', + rooms: ['B315', 'B224', 'B207'], + type: 'MT', + startTime: '12:00', + ), + ), + itemBuilder: (context, index) { + final exam = entry.value[index]; + return TimelineItem( + title: exam.start.day.toString(), + subtitle: exam.start + .shortMonth( + Provider.of(context) + .getLocale(), + ) + .capitalize(), + isActive: _nextExam(exams) == exam, + card: ExamCard( + name: exam.subject, + acronym: exam.subjectAcronym, + rooms: exam.rooms, + type: exam.examType, + startTime: exam.formatTime(exam.start), + isInvisible: hiddenExams.contains(exam.id), + iconAction: () { + setState(() { + if (hiddenExams.contains(exam.id)) { + hiddenExams.remove(exam.id); + } else { + hiddenExams.add(exam.id); + } + + PreferencesController.saveHiddenExams( + hiddenExams, + ); + }); + }, + ), + ); + }, + ), + ], + ), + ), + ) + .toList(), + ), + hasContent: (exams) => exams.isNotEmpty, + onNullContent: Center( + heightFactor: 1.2, + child: ImageLabel( + imagePath: 'assets/images/vacation.png', + label: S.of(context).no_exams_label, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), + sublabel: S.of(context).no_exams, + sublabelTextStyle: const TextStyle(fontSize: 15), + ), + ), + ); + } + + Map> _examsByMonth(List exams) { + final months = >{}; + for (final exam in exams) { + final month = '${exam.start.year}-${exam.start.month}'; + months.putIfAbsent(month, () => []).add(exam); + } + return months; + } + + Exam? _nextExam(List exams) { + final now = DateTime.now(); + final nextExams = exams.where((exam) => exam.start.isAfter(now)).toList() + ..sort((a, b) => a.start.compareTo(b.start)); + return nextExams.isNotEmpty ? nextExams.first : null; + } + + /* + @override + Widget? getTopRightButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: ExamFilterButton( + () => setState(() { + filteredExamTypes = PreferencesController.getFilteredExams(); + }), + ), + ); + } + */ +} diff --git a/packages/uni_app/lib/view/exams/widgets/exam_row.dart b/packages/uni_app/lib/view/exams/widgets/exam_row.dart index abacd5686..1a21e7e70 100644 --- a/packages/uni_app/lib/view/exams/widgets/exam_row.dart +++ b/packages/uni_app/lib/view/exams/widgets/exam_row.dart @@ -39,7 +39,7 @@ class _ExamRowState extends State { @override Widget build(BuildContext context) { final roomsKey = - '${widget.exam.subject}-${widget.exam.rooms}-${widget.exam.startTime}-' + '${widget.exam.subjectAcronym}-${widget.exam.rooms}-${widget.exam.startTime}-' '${widget.exam.finishTime}'; return Center( child: Container( @@ -63,7 +63,7 @@ class _ExamRowState extends State { : [], ), ExamTitle( - subject: widget.exam.subject, + subject: widget.exam.subjectAcronym, type: widget.exam.examType, ), Row( @@ -144,7 +144,7 @@ class _ExamRowState extends State { Event createExamEvent() { return Event( - title: '${widget.exam.examType} ${widget.exam.subject}', + title: '${widget.exam.examType} ${widget.exam.subjectAcronym}', location: widget.exam.rooms.toString(), startDate: widget.exam.start, endDate: widget.exam.finish, diff --git a/packages/uni_app/lib/view/home/widgets/remaining_exams_card.dart b/packages/uni_app/lib/view/home/widgets/remaining_exams_card.dart index 56f1f0180..37e4526e3 100644 --- a/packages/uni_app/lib/view/home/widgets/remaining_exams_card.dart +++ b/packages/uni_app/lib/view/home/widgets/remaining_exams_card.dart @@ -30,7 +30,7 @@ class RemainingExamsWidget extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, ), ExamTitle( - subject: exam.subject, + subject: exam.subjectAcronym, type: exam.examType, reverseOrder: true, ), diff --git a/packages/uni_app/pubspec.lock b/packages/uni_app/pubspec.lock index 04778b73c..e9483f5c4 100644 --- a/packages/uni_app/pubspec.lock +++ b/packages/uni_app/pubspec.lock @@ -1147,6 +1147,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + scrollable_positioned_list: + dependency: transitive + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" sentry: dependency: transitive description: diff --git a/packages/uni_app/test/unit/view/Pages/exams_page_view_test.dart b/packages/uni_app/test/unit/view/Pages/exams_page_view_test.dart index 81555ec7c..892680059 100644 --- a/packages/uni_app/test/unit/view/Pages/exams_page_view_test.dart +++ b/packages/uni_app/test/unit/view/Pages/exams_page_view_test.dart @@ -14,9 +14,11 @@ void main() async { await initTestEnvironment(); group('ExamsPage', () { - const firstExamSubject = 'SOPE'; + const firstExamSubjectAcronym = 'SOPE'; + const firstExamSubject = 'Sistemas Operativos'; const firstExamDate = '2019-09-11'; - const secondExamSubject = 'SDIS'; + const secondExamSubjectAcronym = 'SDIS'; + const secondExamSubject = 'Sistemas Distribuídos'; const secondExamDate = '2019-09-12'; testWidgets('When given an empty list', (tester) async { @@ -37,6 +39,7 @@ void main() async { '1230', firstExamBegin, firstExamEnd, + firstExamSubjectAcronym, firstExamSubject, ['B119', 'B107', 'B205'], 'ER', @@ -63,6 +66,7 @@ void main() async { '1231', firstExamBegin, firstExamEnd, + firstExamSubjectAcronym, firstExamSubject, ['B119', 'B107', 'B205'], 'ER', @@ -74,6 +78,7 @@ void main() async { '1232', secondExamBegin, secondExamEnd, + secondExamSubjectAcronym, secondExamSubject, ['B119', 'B107', 'B205'], 'ER', @@ -109,6 +114,7 @@ void main() async { '1233', firstExamBegin, firstExamEnd, + firstExamSubjectAcronym, firstExamSubject, ['B119', 'B107', 'B205'], 'ER', @@ -120,6 +126,7 @@ void main() async { '1234', secondExamBegin, secondExamEnd, + secondExamSubjectAcronym, secondExamSubject, ['B119', 'B107', 'B205'], 'ER', @@ -154,6 +161,7 @@ void main() async { '1235', firstExamBegin, firstExamEnd, + firstExamSubjectAcronym, firstExamSubject, rooms, 'ER', @@ -165,6 +173,7 @@ void main() async { '1236', secondExamBegin, secondExamEnd, + firstExamSubjectAcronym, firstExamSubject, rooms, 'ER', @@ -176,6 +185,7 @@ void main() async { '1237', thirdExamBegin, thirdExamEnd, + secondExamSubjectAcronym, secondExamSubject, rooms, 'ER', @@ -187,6 +197,7 @@ void main() async { '1238', fourthExamBegin, fourthExamEnd, + secondExamSubjectAcronym, secondExamSubject, rooms, 'ER', diff --git a/packages/uni_app/test/unit/view/Widgets/exam_row_test.dart b/packages/uni_app/test/unit/view/Widgets/exam_row_test.dart index be4483997..6b9c1bac3 100644 --- a/packages/uni_app/test/unit/view/Widgets/exam_row_test.dart +++ b/packages/uni_app/test/unit/view/Widgets/exam_row_test.dart @@ -12,7 +12,8 @@ void main() async { await initTestEnvironment(); group('Exam Row', () { - const subject = 'SOPE'; + const subjectAcronym = 'SOPE'; + const subject = 'Sistemas Operativos'; final start = DateTime( DateTime.now().year, DateTime.now().month, @@ -30,7 +31,16 @@ void main() async { testWidgets('When given a single room', (tester) async { final rooms = ['B315']; - final exam = Exam('1230', start, finish, subject, rooms, '', 'feup'); + final exam = Exam( + '1230', + start, + finish, + subjectAcronym, + subject, + rooms, + '', + 'feup', + ); final widget = ExamRow( exam: exam, teacher: '', @@ -44,7 +54,7 @@ void main() async { await tester.pumpWidget(testableWidget(widget, providers: providers)); await tester.pump(); - final roomsKey = '$subject-$rooms-$startTime-$finishTime'; + final roomsKey = '$subjectAcronym-$rooms-$startTime-$finishTime'; expect( find.descendant( @@ -57,7 +67,16 @@ void main() async { testWidgets('When multiple rooms', (tester) async { final rooms = ['B315', 'B316', 'B330']; - final exam = Exam('1230', start, finish, subject, rooms, '', 'feup'); + final exam = Exam( + '1230', + start, + finish, + subjectAcronym, + subject, + rooms, + '', + 'feup', + ); final widget = ExamRow( exam: exam, teacher: '', @@ -72,7 +91,7 @@ void main() async { await tester.pumpWidget(testableWidget(widget, providers: providers)); await tester.pump(); - final roomsKey = '$subject-$rooms-$startTime-$finishTime'; + final roomsKey = '$subjectAcronym-$rooms-$startTime-$finishTime'; expect( find.descendant( diff --git a/packages/uni_ui/lib/cards/exam_card.dart b/packages/uni_ui/lib/cards/exam_card.dart index 218535a94..cc92f949e 100644 --- a/packages/uni_ui/lib/cards/exam_card.dart +++ b/packages/uni_ui/lib/cards/exam_card.dart @@ -25,6 +25,13 @@ class ExamCard extends StatelessWidget { final bool showIcon; final Function()? iconAction; + static const Map examTypeColors = { + 'MT': BadgeColors.mt, + 'EN': BadgeColors.en, + 'ER': BadgeColors.er, + 'EE': BadgeColors.ee, + }; + @override Widget build(BuildContext context) { return Opacity( @@ -47,7 +54,7 @@ class ExamCard extends StatelessWidget { const SizedBox(width: 8), Badge( label: Text(type), - backgroundColor: BadgeColors.er, + backgroundColor: examTypeColors[type], textColor: Theme.of(context).colorScheme.surface, ), ], diff --git a/packages/uni_ui/lib/card_timeline.dart b/packages/uni_ui/lib/cards/timeline_card.dart similarity index 92% rename from packages/uni_ui/lib/card_timeline.dart rename to packages/uni_ui/lib/cards/timeline_card.dart index 6bb44958e..88e8d705a 100644 --- a/packages/uni_ui/lib/card_timeline.dart +++ b/packages/uni_ui/lib/cards/timeline_card.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; class TimelineItem extends StatelessWidget { const TimelineItem( - {required this.startTime, - required this.endTime, + {required this.title, + required this.subtitle, required this.card, this.isActive = false, super.key}); - final String startTime; - final String endTime; + final String title; + final String subtitle; final Widget card; final bool isActive; @@ -20,9 +20,9 @@ class TimelineItem extends StatelessWidget { width: 50, child: Column( children: [ - Text(startTime, + Text(title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), - Text(endTime, + Text(subtitle, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)) ], ), diff --git a/packages/uni_ui/lib/icons.dart b/packages/uni_ui/lib/icons.dart new file mode 100644 index 000000000..72ee466b8 --- /dev/null +++ b/packages/uni_ui/lib/icons.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +// A list of all available icons +class UniIcons { + static const lecture = PhosphorIconsDuotone.lectern; + static const exam = PhosphorIconsDuotone.exam; + static const course = PhosphorIconsDuotone.certificate; +} + +// The same as default Icon class from material.dart but allowing to use PhosphorIcons duotone icons +class UniIcon extends PhosphorIcon { + const UniIcon( + IconData icon, { + super.key, + double size = 24, + Color? color, + String? semanticLabel, + TextDirection? textDirection, + }) : super( + icon, + size: size, + color: color, + semanticLabel: semanticLabel, + textDirection: textDirection, + ); +} diff --git a/packages/uni_ui/lib/tabs/tab_icon.dart b/packages/uni_ui/lib/tabs/tab_icon.dart new file mode 100644 index 000000000..9b45aa9e8 --- /dev/null +++ b/packages/uni_ui/lib/tabs/tab_icon.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:uni_ui/icons.dart'; + +class TabIcon extends StatelessWidget { + const TabIcon({ + super.key, + required this.icon, + required this.text, + }); + + final IconData icon; + final String text; + + @override + Widget build(BuildContext context) { + return Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + UniIcon(icon), + const SizedBox(width: 4), + Text(text), + ], + ), + ); + } +}