diff --git a/uni/android/build.gradle b/uni/android/build.gradle index db859f3a9..602681ca4 100644 --- a/uni/android/build.gradle +++ b/uni/android/build.gradle @@ -30,4 +30,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/uni/lib/controller/fetchers/library_reservation_fetcher.dart b/uni/lib/controller/fetchers/library_reservation_fetcher.dart new file mode 100644 index 000000000..f6a0bfebd --- /dev/null +++ b/uni/lib/controller/fetchers/library_reservation_fetcher.dart @@ -0,0 +1,23 @@ +import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; +import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/controller/parsers/parser_library_reservation.dart'; +import 'package:uni/model/entities/library_reservation.dart'; +import 'package:uni/model/entities/session.dart'; + +/// Get the library rooms' reservations from the website +class LibraryReservationsFetcherHtml implements SessionDependantFetcher { + @override + List getEndpoints(Session session) { + final url = '${NetworkRouter.getBaseUrl('feup')}res_recursos_geral' + '.pedidos_list?pct_tipo_grupo_id=3'; + return [url]; + } + + Future> getReservations(Session session) async { + final baseUrl = getEndpoints(session)[0]; + final response = NetworkRouter.getWithCookies(baseUrl, {}, session); + final reservations = await response.then(getReservationsFromHtml); + + return reservations; + } +} diff --git a/uni/lib/controller/local_storage/database/app_library_reservation.dart b/uni/lib/controller/local_storage/database/app_library_reservation.dart new file mode 100644 index 000000000..63967ae1a --- /dev/null +++ b/uni/lib/controller/local_storage/database/app_library_reservation.dart @@ -0,0 +1,41 @@ +import 'package:uni/controller/local_storage/database/app_database.dart'; +import 'package:uni/model/entities/library_reservation.dart'; + +class LibraryReservationDatabase extends AppDatabase { + LibraryReservationDatabase() + : super('reservations.db', [ + ''' + CREATE TABLE RESERVATION( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room TEXT, + startDate TEXT, + duration INT + ) + ''' + ]); + + Future saveReservations(List reservations) async { + final db = await getDatabase(); + await db.transaction((txn) async { + await txn.delete('RESERVATION'); + for (final reservation in reservations) { + await txn.insert('RESERVATION', reservation.toMap()); + } + }); + } + + Future> reservations() async { + final db = await getDatabase(); + + final List> items = await db.query('RESERVATION'); + + return items.map((item) { + final minutes = item['duration'] as int; + return LibraryReservation( + item['room'] as String, + DateTime.parse(item['startDate'] as String), + Duration(hours: minutes ~/ 60, minutes: minutes % 60), + ); + }).toList(); + } +} diff --git a/uni/lib/controller/parsers/parser_library_reservation.dart b/uni/lib/controller/parsers/parser_library_reservation.dart new file mode 100644 index 000000000..aec82c7dc --- /dev/null +++ b/uni/lib/controller/parsers/parser_library_reservation.dart @@ -0,0 +1,24 @@ +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:uni/model/entities/library_reservation.dart'; + +Future> getReservationsFromHtml( + Response response, +) async { + final document = parse(response.body); + + final reservationHtml = document.getElementsByClassName('d interior'); + + return reservationHtml.map((element) { + final room = element.children[5].firstChild?.text; + final date = element.children[0].firstChild?.text; + final hour = element.children[2].firstChild?.text; + final startDate = DateTime.parse('$date $hour'); + final durationHtml = element.children[4].firstChild?.text; + final duration = Duration( + hours: int.parse(durationHtml!.substring(0, 2)), + minutes: int.parse(durationHtml.substring(3, 5)), + ); + return LibraryReservation(room!, startDate, duration); + }).toList(); +} diff --git a/uni/lib/generated/intl/messages_en.dart b/uni/lib/generated/intl/messages_en.dart index de7261274..7215972f1 100644 --- a/uni/lib/generated/intl/messages_en.dart +++ b/uni/lib/generated/intl/messages_en.dart @@ -7,8 +7,7 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes +// ignore_for_file:unused_import, file_names import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -20,12 +19,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; - static String m0(time) => "last refresh at ${time}"; + static m0(time) => "last refresh at ${time}"; - static String m1(time) => + static m1(time) => "${Intl.plural(time, zero: 'Refreshed ${time} minutes ago', one: 'Refreshed ${time} minute ago', other: 'Refreshed ${time} minutes ago')}"; - static String m2(title) => "${Intl.select(title, { + static m2(title) => "${Intl.select(title, { 'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', @@ -42,7 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { })}"; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { + static _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About us"), "academic_services": MessageLookupByLibrary.simpleMessage("Academic services"), @@ -152,8 +151,15 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Language"), "last_refresh_time": m0, "last_timestamp": m1, + "library": MessageLookupByLibrary.simpleMessage("Library"), "library_occupation": MessageLookupByLibrary.simpleMessage("Library Occupation"), + "library_reservations": + MessageLookupByLibrary.simpleMessage("Library Reservations"), + "library_tab_occupation": + MessageLookupByLibrary.simpleMessage("Occupation"), + "library_tab_reservations": + MessageLookupByLibrary.simpleMessage("Reservations"), "load_error": MessageLookupByLibrary.simpleMessage( "Error loading the information"), "loading_terms": MessageLookupByLibrary.simpleMessage( @@ -212,6 +218,8 @@ class MessageLookup extends MessageLookupByLibrary { "No print balance information"), "no_references": MessageLookupByLibrary.simpleMessage( "There are no references to pay"), + "no_reservations": MessageLookupByLibrary.simpleMessage( + "There are no reservations to display"), "no_results": MessageLookupByLibrary.simpleMessage("No match"), "no_selected_courses": MessageLookupByLibrary.simpleMessage( "There are no course units to display"), diff --git a/uni/lib/generated/intl/messages_pt_PT.dart b/uni/lib/generated/intl/messages_pt_PT.dart index 76da6d9ff..8bee9bb5e 100644 --- a/uni/lib/generated/intl/messages_pt_PT.dart +++ b/uni/lib/generated/intl/messages_pt_PT.dart @@ -7,8 +7,7 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes +// ignore_for_file:unused_import, file_names import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -20,12 +19,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt_PT'; - static String m0(time) => "última atualização às ${time}"; + static m0(time) => "última atualização às ${time}"; - static String m1(time) => + static m1(time) => "${Intl.plural(time, zero: 'Atualizado há ${time} minutos', one: 'Atualizado há ${time} minuto', other: 'Atualizado há ${time} minutos')}"; - static String m2(title) => "${Intl.select(title, { + static m2(title) => "${Intl.select(title, { 'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', @@ -152,8 +151,15 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Idioma"), "last_refresh_time": m0, "last_timestamp": m1, + "library": MessageLookupByLibrary.simpleMessage("Biblioteca"), "library_occupation": MessageLookupByLibrary.simpleMessage("Ocupação da Biblioteca"), + "library_reservations": + MessageLookupByLibrary.simpleMessage("Gabinetes Reservados"), + "library_tab_occupation": + MessageLookupByLibrary.simpleMessage("Ocupação"), + "library_tab_reservations": + MessageLookupByLibrary.simpleMessage("Gabinetes"), "load_error": MessageLookupByLibrary.simpleMessage( "Erro ao carregar a informação"), "loading_terms": MessageLookupByLibrary.simpleMessage( @@ -214,6 +220,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sem informação de saldo"), "no_references": MessageLookupByLibrary.simpleMessage( "Não existem referências a pagar"), + "no_reservations": MessageLookupByLibrary.simpleMessage( + "Não existem reservas para apresentar"), "no_results": MessageLookupByLibrary.simpleMessage("Sem resultados"), "no_selected_courses": MessageLookupByLibrary.simpleMessage( "Não existem cadeiras para apresentar"), diff --git a/uni/lib/generated/l10n.dart b/uni/lib/generated/l10n.dart index 30773b6a0..0363deb2c 100644 --- a/uni/lib/generated/l10n.dart +++ b/uni/lib/generated/l10n.dart @@ -10,7 +10,7 @@ import 'intl/messages_all.dart'; // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each -// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes +// ignore_for_file: avoid_redundant_argument_values class S { S(); @@ -753,6 +753,16 @@ class S { ); } + /// `Library` + String get library { + return Intl.message( + 'Library', + name: 'library', + desc: '', + args: [], + ); + } + /// `Library Occupation` String get library_occupation { return Intl.message( @@ -763,6 +773,36 @@ class S { ); } + /// `Library Reservations` + String get library_reservations { + return Intl.message( + 'Library Reservations', + name: 'library_reservations', + desc: '', + args: [], + ); + } + + /// `Occupation` + String get library_tab_occupation { + return Intl.message( + 'Occupation', + name: 'library_tab_occupation', + desc: '', + args: [], + ); + } + + /// `Reservations` + String get library_tab_reservations { + return Intl.message( + 'Reservations', + name: 'library_tab_reservations', + desc: '', + args: [], + ); + } + /// `Error downloading the file` String get download_error { return Intl.message( @@ -898,6 +938,16 @@ class S { ); } + /// `There are no reservations to display` + String get no_reservations { + return Intl.message( + 'There are no reservations to display', + name: 'no_reservations', + desc: '', + args: [], + ); + } + /// `There are no classes to display` String get no_class { return Intl.message( diff --git a/uni/lib/l10n/intl_en.arb b/uni/lib/l10n/intl_en.arb index bf8d2121c..d26a70e7d 100644 --- a/uni/lib/l10n/intl_en.arb +++ b/uni/lib/l10n/intl_en.arb @@ -148,8 +148,16 @@ "time": {} } }, + "library": "Library", + "@library": {}, "library_occupation": "Library Occupation", "@library_occupation": {}, + "library_reservations": "Library Reservations", + "@library_reservations": {}, + "library_tab_occupation": "Occupation", + "@library_tab_occupation": {}, + "library_tab_reservations": "Reservations", + "@library_tab_reservations": {}, "download_error": "Error downloading the file", "@download_error": {}, "loading_terms": "Loading Terms and Conditions...", @@ -174,6 +182,8 @@ "@no_bus": {}, "no_bus_stops": "No configured stops", "@no_bus_stops": {}, + "no_reservations": "There are no reservations to display", + "@no_reservations": {}, "no_class": "There are no classes to display", "@no_class": {}, "no_classes": "No classes to present", diff --git a/uni/lib/l10n/intl_pt_PT.arb b/uni/lib/l10n/intl_pt_PT.arb index 56dbd3945..bbf6625e6 100644 --- a/uni/lib/l10n/intl_pt_PT.arb +++ b/uni/lib/l10n/intl_pt_PT.arb @@ -150,10 +150,18 @@ "time": {} } }, - "load_error": "Erro ao carregar a informação", - "@load_error": {}, + "library": "Biblioteca", + "@library": {}, "library_occupation": "Ocupação da Biblioteca", "@library_occupation": {}, + "library_reservations": "Gabinetes Reservados", + "@library_reservations": {}, + "library_tab_occupation": "Ocupação", + "@library_tab_occupation": {}, + "library_tab_reservations": "Gabinetes", + "@library_tab_reservations": {}, + "load_error": "Erro ao carregar a informação", + "@load_error": {}, "download_error": "Erro ao descarregar o ficheiro", "@download_error": {}, "successful_open": "Ficheiro aberto com sucesso", @@ -184,6 +192,8 @@ "@no_bus": {}, "no_bus_stops": "Não existe nenhuma paragem configurada", "@no_bus_stops": {}, + "no_reservations": "Não existem reservas para apresentar", + "@no_reservations": {}, "no_class": "Não existem turmas para apresentar", "@no_class": {}, "no_classes": "Não existem aulas para apresentar", diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 21dfe91fd..68c87c47e 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -23,6 +23,7 @@ import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/library_reservations_provider.dart'; import 'package:uni/model/providers/lazy/reference_provider.dart'; import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/model/providers/plausible/plausible_provider.dart'; @@ -81,6 +82,7 @@ Future main() async { SessionProvider(), CalendarProvider(), LibraryOccupationProvider(), + LibraryReservationsProvider(), FacultyLocationsProvider(), ReferenceProvider(), ); @@ -159,6 +161,9 @@ Future main() async { ChangeNotifierProvider( create: (context) => stateProviders.libraryOccupationProvider, ), + ChangeNotifierProvider( + create: (context) => stateProviders.libraryReservationsProvider, + ), ChangeNotifierProvider( create: (context) => stateProviders.facultyLocationsProvider, ), @@ -273,11 +278,18 @@ class ApplicationState extends State { page: const CalendarPageView(), settings: settings, ), - '/${NavigationItem.navLibrary.route}': + '/${NavigationItem.navLibraryOccupation.route}': PageTransition.makePageTransition( page: const LibraryPage(), settings: settings, ), + '/${NavigationItem.navLibraryReservations.route}': + PageTransition.makePageTransition( + page: const LibraryPage( + startTab: LibraryPageTab.reservations, + ), + settings: settings, + ), '/${NavigationItem.navFaculty.route}': PageTransition.makePageTransition( page: const FacultyPageView(), diff --git a/uni/lib/model/entities/library_reservation.dart b/uni/lib/model/entities/library_reservation.dart new file mode 100644 index 000000000..8dcffd2d7 --- /dev/null +++ b/uni/lib/model/entities/library_reservation.dart @@ -0,0 +1,33 @@ +// TO DO: Change this after #1072 is merged +/// Private room reservation from the library +class LibraryReservation { + LibraryReservation(this.room, this.startDate, this.duration); + final String room; + final DateTime startDate; + final Duration duration; + + Map toMap() { + final map = { + 'room': room, + 'startDate': startDate.toIso8601String(), + 'duration': duration.inMinutes, + }; + return map; + } + + @override + String toString() { + return '$room, $startDate, $duration'; + } + + @override + bool operator ==(Object other) { + return other is LibraryReservation && + room == other.room && + (startDate.compareTo(other.startDate) == 0) && + (duration.compareTo(other.duration) == 0); + } + + @override + int get hashCode => Object.hash(room, startDate, duration); +} diff --git a/uni/lib/model/providers/lazy/library_reservations_provider.dart b/uni/lib/model/providers/lazy/library_reservations_provider.dart new file mode 100644 index 000000000..f0fbd1c19 --- /dev/null +++ b/uni/lib/model/providers/lazy/library_reservations_provider.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:uni/controller/fetchers/library_reservation_fetcher.dart'; +import 'package:uni/controller/local_storage/database/app_library_reservation.dart'; +import 'package:uni/model/entities/library_reservation.dart'; +import 'package:uni/model/providers/state_provider_notifier.dart'; +import 'package:uni/model/providers/state_providers.dart'; + +class LibraryReservationsProvider + extends StateProviderNotifier> { + LibraryReservationsProvider() + : super(dependsOnSession: true, cacheDuration: const Duration(hours: 1)); + List? _reservations; + + List get reservations => _reservations ?? []; + + @override + Future> loadFromStorage( + StateProviders stateProviders, + ) { + final db = LibraryReservationDatabase(); + return db.reservations(); + } + + @override + Future> loadFromRemote( + StateProviders stateProviders, + ) async { + final session = stateProviders.sessionProvider.state!; + final reservations = + await LibraryReservationsFetcherHtml().getReservations(session); + + final db = LibraryReservationDatabase(); + unawaited(db.saveReservations(reservations)); + + return reservations; + } +} diff --git a/uni/lib/model/providers/state_providers.dart b/uni/lib/model/providers/state_providers.dart index 96a94dd26..e42e42f34 100644 --- a/uni/lib/model/providers/state_providers.dart +++ b/uni/lib/model/providers/state_providers.dart @@ -9,6 +9,7 @@ import 'package:uni/model/providers/lazy/exam_provider.dart'; import 'package:uni/model/providers/lazy/faculty_locations_provider.dart'; import 'package:uni/model/providers/lazy/lecture_provider.dart'; import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/model/providers/lazy/library_reservations_provider.dart'; import 'package:uni/model/providers/lazy/reference_provider.dart'; import 'package:uni/model/providers/lazy/restaurant_provider.dart'; import 'package:uni/model/providers/startup/profile_provider.dart'; @@ -25,6 +26,7 @@ class StateProviders { this.sessionProvider, this.calendarProvider, this.libraryOccupationProvider, + this.libraryReservationsProvider, this.facultyLocationsProvider, this.referenceProvider, ); @@ -43,6 +45,7 @@ class StateProviders { SessionProvider(), CalendarProvider(), LibraryOccupationProvider(), + LibraryReservationsProvider(), FacultyLocationsProvider(), ReferenceProvider(), ); @@ -65,6 +68,8 @@ class StateProviders { Provider.of(context, listen: false); final libraryOccupationProvider = Provider.of(context, listen: false); + final libraryReservationsProvider = + Provider.of(context, listen: false); final facultyLocationsProvider = Provider.of(context, listen: false); final referenceProvider = @@ -80,6 +85,7 @@ class StateProviders { sessionProvider, calendarProvider, libraryOccupationProvider, + libraryReservationsProvider, facultyLocationsProvider, referenceProvider, ); @@ -94,6 +100,7 @@ class StateProviders { final SessionProvider sessionProvider; final CalendarProvider calendarProvider; final LibraryOccupationProvider libraryOccupationProvider; + final LibraryReservationsProvider libraryReservationsProvider; final FacultyLocationsProvider facultyLocationsProvider; final ReferenceProvider referenceProvider; @@ -107,6 +114,7 @@ class StateProviders { sessionProvider.invalidate(); calendarProvider.invalidate(); libraryOccupationProvider.invalidate(); + libraryReservationsProvider.invalidate(); facultyLocationsProvider.invalidate(); referenceProvider.invalidate(); } diff --git a/uni/lib/utils/favorite_widget_type.dart b/uni/lib/utils/favorite_widget_type.dart index 9e27fe865..6a830ee23 100644 --- a/uni/lib/utils/favorite_widget_type.dart +++ b/uni/lib/utils/favorite_widget_type.dart @@ -4,6 +4,7 @@ enum FavoriteWidgetType { printBalance, account, libraryOccupation(faculties: {'feup'}), + libraryReservations(faculties: {'feup'}), busStops, restaurant; diff --git a/uni/lib/utils/navigation_items.dart b/uni/lib/utils/navigation_items.dart index 67078affc..95bbd3d24 100644 --- a/uni/lib/utils/navigation_items.dart +++ b/uni/lib/utils/navigation_items.dart @@ -7,6 +7,8 @@ enum NavigationItem { navLocations('locais', faculties: {'feup'}), navRestaurants('restaurantes'), navCalendar('calendario'), + navLibraryOccupation('biblioteca', faculties: {'feup'}), + navLibraryReservations('reservas', faculties: {'feup'}), navLibrary('biblioteca', faculties: {'feup'}), navFaculty('faculdade'), navAcademicPath('percurso_academico'), diff --git a/uni/lib/view/faculty/faculty.dart b/uni/lib/view/faculty/faculty.dart index 5db7bf9b0..2015d409f 100644 --- a/uni/lib/view/faculty/faculty.dart +++ b/uni/lib/view/faculty/faculty.dart @@ -14,6 +14,7 @@ import 'package:uni/view/faculty/widgets/multimedia_center_card.dart'; import 'package:uni/view/faculty/widgets/other_links_card.dart'; import 'package:uni/view/faculty/widgets/sigarra_links_card.dart'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; +import 'package:uni/view/library/widgets/library_reservations_card.dart'; class FacultyPageView extends StatefulWidget { const FacultyPageView({super.key}); @@ -33,6 +34,7 @@ class FacultyPageViewState extends GeneralPageViewState { children: [ LibraryOccupationCard(), CalendarCard(), + LibraryReservationsCard(), ...getUtilsSection(), ], ); diff --git a/uni/lib/view/home/widgets/main_cards_list.dart b/uni/lib/view/home/widgets/main_cards_list.dart index ad9167dbc..5fda2b4bd 100644 --- a/uni/lib/view/home/widgets/main_cards_list.dart +++ b/uni/lib/view/home/widgets/main_cards_list.dart @@ -11,6 +11,7 @@ import 'package:uni/view/home/widgets/exit_app_dialog.dart'; import 'package:uni/view/home/widgets/restaurant_card.dart'; import 'package:uni/view/home/widgets/schedule_card.dart'; import 'package:uni/view/library/widgets/library_occupation_card.dart'; +import 'package:uni/view/library/widgets/library_reservations_card.dart'; import 'package:uni/view/profile/widgets/account_info_card.dart'; import 'package:uni/view/profile/widgets/print_info_card.dart'; @@ -39,6 +40,8 @@ class MainCardsList extends StatefulWidget { FavoriteWidgetType.restaurant: RestaurantCard.fromEditingInformation, FavoriteWidgetType.libraryOccupation: LibraryOccupationCard.fromEditingInformation, + FavoriteWidgetType.libraryReservations: + LibraryReservationsCard.fromEditingInformation, }; @override diff --git a/uni/lib/view/library/library.dart b/uni/lib/view/library/library.dart index a86bc19c0..7b1e2d721 100644 --- a/uni/lib/view/library/library.dart +++ b/uni/lib/view/library/library.dart @@ -1,128 +1,73 @@ import 'package:flutter/material.dart'; -import 'package:percent_indicator/linear_percent_indicator.dart'; -import 'package:provider/provider.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/model/entities/library_occupation.dart'; -import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; import 'package:uni/utils/navigation_items.dart'; -import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; -import 'package:uni/view/lazy_consumer.dart'; -import 'package:uni/view/library/widgets/library_occupation_card.dart'; +import 'package:uni/view/library/widgets/library_occupation_tab.dart'; +import 'package:uni/view/library/widgets/library_reservations_tab.dart'; + +enum LibraryPageTab { + occupation, + reservations, +} class LibraryPage extends StatefulWidget { - const LibraryPage({super.key}); + const LibraryPage({this.startTab = LibraryPageTab.occupation, super.key}); + final LibraryPageTab startTab; @override State createState() => LibraryPageState(); } class LibraryPageState extends SecondaryPageViewState { - @override - Widget getBody(BuildContext context) { - return ListView( - shrinkWrap: true, - children: [ - LibraryOccupationCard(), - PageTitle(name: S.of(context).floors), - LazyConsumer( - builder: getFloorRows, - hasContent: (occupation) => occupation.floors.isNotEmpty, - onNullContent: Center( - child: Text( - S.of(context).no_library_info, - style: const TextStyle(fontSize: 18), - ), - ), - contentLoadingWidget: const CircularProgressIndicator(), - ), - ], - ); - } + late TabController tabController; + late List tabs; + static const List tabsContent = [ + LibraryOccupationTab(), + LibraryReservationsTab(), + ]; - // This will lazy consume - Widget getFloorRows(BuildContext context, LibraryOccupation occupation) { - final floors = []; - for (var i = 1; i < occupation.floors.length; i += 2) { - floors.add( - createFloorRow( - context, - occupation.getFloor(i), - occupation.getFloor(i + 1), - ), - ); + @override + Future onRefresh(BuildContext context) { + if (tabController.index == 0) { + return (tabsContent[0] as LibraryOccupationTab).refresh(context); + } else { + return (tabsContent[1] as LibraryReservationsTab).refresh(context); } - return Column( - children: floors, - ); } - Widget createFloorRow( - 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, - width: 150, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - 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( - '${S.of(context).floor} ${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.secondary), - ), - LinearPercentIndicator( - lineHeight: 7, - percent: floor.percentage / 100, - progressColor: Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).dividerColor, - ), - ], + @override + Widget getBody(BuildContext context) { + tabs = [ + Tab(text: S.of(context).library_tab_occupation), + Tab(text: S.of(context).library_tab_reservations), + ]; + return DefaultTabController( + length: tabs.length, + child: Builder( + builder: (BuildContext builderContext) { + tabController = DefaultTabController.of(builderContext); + tabController.index = + (widget.startTab == LibraryPageTab.occupation) ? 0 : 1; + return Column( + children: [ + TabBar( + controller: tabController, + physics: const BouncingScrollPhysics(), + tabs: tabs, + ), + Expanded( + child: TabBarView( + controller: tabController, + children: tabsContent, + ), + ), + ], + ); + }, ), ); } - @override - Future onRefresh(BuildContext context) { - return Provider.of(context, listen: false) - .forceRefresh(context); - } - @override String? getTitle() => S.of(context).nav_title(NavigationItem.navLibrary.route); diff --git a/uni/lib/view/library/widgets/library_occupation_card.dart b/uni/lib/view/library/widgets/library_occupation_card.dart index d167efdf2..f7a4b2a6c 100644 --- a/uni/lib/view/library/widgets/library_occupation_card.dart +++ b/uni/lib/view/library/widgets/library_occupation_card.dart @@ -22,8 +22,10 @@ class LibraryOccupationCard extends GenericCard { String getTitle(BuildContext context) => S.of(context).library_occupation; @override - Future onClick(BuildContext context) => - Navigator.pushNamed(context, '/${NavigationItem.navLibrary.route}'); + Future onClick(BuildContext context) => Navigator.pushNamed( + context, + '/${NavigationItem.navLibraryOccupation.route}', + ); @override void onRefresh(BuildContext context) { diff --git a/uni/lib/view/library/widgets/library_occupation_tab.dart b/uni/lib/view/library/widgets/library_occupation_tab.dart new file mode 100644 index 000000000..8be6a13e6 --- /dev/null +++ b/uni/lib/view/library/widgets/library_occupation_tab.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/library_occupation.dart'; +import 'package:uni/model/providers/lazy/library_occupation_provider.dart'; +import 'package:uni/view/common_widgets/page_title.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/library/widgets/library_occupation_card.dart'; + +class LibraryOccupationTab extends StatefulWidget { + const LibraryOccupationTab({super.key}); + + @override + LibraryOccupationTabState createState() => LibraryOccupationTabState(); + + Future refresh(BuildContext context) async { + await Provider.of(context, listen: false) + .forceRefresh(context); + } +} + +class LibraryOccupationTabState extends State { + @override + Widget build(BuildContext context) { + return LazyConsumer( + builder: (context, occupation) { + return LibraryOccupationTabView(occupation); + }, + contentLoadingWidget: const Center(child: CircularProgressIndicator()), + hasContent: (occupation) => occupation.floors.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_data, + style: const TextStyle(fontSize: 18), + ), + ), + ); + } +} + +class LibraryOccupationTabView extends StatelessWidget { + const LibraryOccupationTabView(this.occupation, {super.key}); + final LibraryOccupation? occupation; + + @override + Widget build(BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + LibraryOccupationCard(), + if (occupation != null) ...[ + PageTitle(name: S.of(context).floors), + FloorRows(occupation!), + ], + ], + ); + } +} + +class FloorRows extends StatelessWidget { + const FloorRows(this.occupation, {super.key}); + final LibraryOccupation occupation; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 25), + crossAxisSpacing: 25, + mainAxisSpacing: 5, + physics: const NeverScrollableScrollPhysics(), + children: occupation.floors.map(FloorCard.new).toList(), + ); + } +} + +class FloorCard extends StatelessWidget { + const FloorCard(this.floor, {super.key}); + final FloorOccupation floor; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10), + height: 150, + width: 150, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + 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( + '${S.of(context).floor} ${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.secondary), + ), + 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_reservations_card.dart b/uni/lib/view/library/widgets/library_reservations_card.dart new file mode 100644 index 000000000..fcfd2ca3f --- /dev/null +++ b/uni/lib/view/library/widgets/library_reservations_card.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/library_reservation.dart'; +import 'package:uni/model/providers/lazy/library_reservations_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/common_widgets/generic_card.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/library/widgets/reservation_row.dart'; + +class LibraryReservationsCard extends GenericCard { + LibraryReservationsCard({super.key}); + + const LibraryReservationsCard.fromEditingInformation( + super.key, { + required super.editingMode, + super.onDelete, + }) : super.fromEditingInformation(); + + @override + Future onClick(BuildContext context) => Navigator.pushNamed( + context, + '/${NavigationItem.navLibraryReservations.route}', + ); + + @override + String getTitle(BuildContext context) => S.of(context).library_reservations; + + @override + void onRefresh(BuildContext context) { + Provider.of(context, listen: false) + .forceRefresh(context); + } + + @override + Widget buildCardContent(BuildContext context) { + return LazyConsumer>( + builder: (context, reservations) { + return RoomsList(reservations); + }, + contentLoadingWidget: const Center(child: CircularProgressIndicator()), + hasContent: (reservations) => reservations.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_reservations, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ); + } +} + +class RoomsList extends StatelessWidget { + const RoomsList(this.reservations, {super.key}); + final List reservations; + + @override + Widget build(BuildContext context) { + if (reservations.isEmpty) { + return Center( + child: Text( + S.of(context).no_data, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ); + } + + final limitedReservations = reservations.take(2); + return Column( + children: limitedReservations.map((reservation) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 0.5, + ), + borderRadius: const BorderRadius.all(Radius.circular(7)), + ), + margin: const EdgeInsets.all(8), + child: ReservationRow(reservation), + ); + }).toList(), + ); + } +} diff --git a/uni/lib/view/library/widgets/library_reservations_tab.dart b/uni/lib/view/library/widgets/library_reservations_tab.dart new file mode 100644 index 000000000..5a607314e --- /dev/null +++ b/uni/lib/view/library/widgets/library_reservations_tab.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/library_reservation.dart'; +import 'package:uni/model/providers/lazy/library_reservations_provider.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/library/widgets/reservation_row.dart'; + +class LibraryReservationsTab extends StatelessWidget { + const LibraryReservationsTab({super.key}); + + @override + Widget build(BuildContext context) { + return LazyConsumer>( + builder: (context, reservations) { + return LibraryReservationsTabView(reservations); + }, + contentLoadingWidget: const Center(child: CircularProgressIndicator()), + hasContent: (reservations) => reservations.isNotEmpty, + onNullContent: Center( + child: Text( + S.of(context).no_reservations, + style: const TextStyle(fontSize: 18), + ), + ), + ); + } + + Future refresh(BuildContext context) async { + await Provider.of(context, listen: false) + .forceRefresh(context); + } +} + +class LibraryReservationsTabView extends StatelessWidget { + const LibraryReservationsTabView(this.reservations, {super.key}); + final List reservations; + + @override + Widget build(BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + LibraryReservationsList(reservations), + ], + ); + } +} + +class LibraryReservationsList extends StatelessWidget { + const LibraryReservationsList(this.reservations, {super.key}); + final List reservations; + + @override + Widget build(BuildContext context) { + return Column( + children: reservations + .map( + (reservation) => Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + margin: const EdgeInsets.all(8), + child: ReservationRow(reservation), + ), + ) + .toList(), + ); + } +} diff --git a/uni/lib/view/library/widgets/reservation_row.dart b/uni/lib/view/library/widgets/reservation_row.dart new file mode 100644 index 000000000..d79ef3ddb --- /dev/null +++ b/uni/lib/view/library/widgets/reservation_row.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/library_reservation.dart'; +import 'package:uni/view/locale_notifier.dart'; + +class ReservationRow extends StatelessWidget { + ReservationRow(this.reservation, {super.key}) { + initializeDateFormatting(); + } + final LibraryReservation reservation; + + @override + Widget build(BuildContext context) { + final day = DateFormat('dd').format(reservation.startDate); + final month = DateFormat('MMMM', 'pt').format(reservation.startDate); + final weekdays = + Provider.of(context).getWeekdaysWithLocale(); + final weekDay = weekdays[reservation.startDate.weekday - 1]; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text( + DateFormat('HH:mm').format(reservation.startDate), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + DateFormat('HH:mm') + .format(reservation.startDate.add(reservation.duration)), + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + Column( + children: [ + Text( + reservation.room, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.apply(color: Theme.of(context).colorScheme.tertiary), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 2)), + Text( + '$weekDay, $day ${S.of(context).of_month} $month', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const ReservationRemoveButton(), + ], + ); + } +} + +class ReservationRemoveButton extends StatelessWidget { + const ReservationRemoveButton({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton( + constraints: const BoxConstraints( + minHeight: kMinInteractiveDimension / 3, + minWidth: kMinInteractiveDimension / 3, + ), + icon: const Icon(Icons.close), + iconSize: 24, + color: Colors.grey, + alignment: Alignment.centerRight, + onPressed: () => {}, + ); + } +} diff --git a/uni/pubspec.lock b/uni/pubspec.lock index 27418c39d..5a53f168b 100644 --- a/uni/pubspec.lock +++ b/uni/pubspec.lock @@ -221,10 +221,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -253,10 +253,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.1" crypto: dependency: "direct main" description: @@ -581,6 +581,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + url: "https://pub.dev" + source: hosted + version: "9.0.16" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + url: "https://pub.dev" + source: hosted + version: "1.0.5" lists: dependency: transitive description: @@ -625,10 +641,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" material_design_icons_flutter: dependency: "direct main" description: @@ -641,10 +657,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mgrs_dart: dependency: transitive description: @@ -738,10 +754,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1087,18 +1103,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1135,26 +1151,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" timelines: dependency: "direct main" description: @@ -1335,10 +1351,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -1351,10 +1367,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.4.0" web_socket_channel: dependency: transitive description: @@ -1420,5 +1436,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.3 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.7"