From be398320402f4db70419e48d3bd437edefd3a132 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sat, 23 Sep 2023 19:33:38 +0100 Subject: [PATCH 01/21] Download feature --- .../course_units_info_fetcher.dart | 22 +++++++++ .../parsers/parser_course_unit_info.dart | 33 ++++++++++++++ .../controller/parsers/parser_schedule.dart | 2 + .../course_units/course_unit_file.dart | 8 ++++ .../lazy/course_units_info_provider.dart | 21 +++++++++ .../course_unit_info/course_unit_info.dart | 45 ++++++++++++++++++- .../widgets/course_unit_files.dart | 40 +++++++++++++++++ 7 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 uni/lib/model/entities/course_units/course_unit_file.dart create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_files.dart diff --git a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart index 608a23f6c..0dd3caa3b 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 @@ -5,6 +5,8 @@ import 'package:uni/controller/parsers/parser_course_unit_info.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:http/http.dart'; class CourseUnitsInfoFetcher implements SessionDependantFetcher { @override @@ -26,6 +28,26 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseCourseUnitSheet(response); } + Future> fetchCourseUnitFiles( + Session session, + int occurId, + ) async { + final urls = + getEndpoints(session).map((url) => '${url}mob_ucurr_geral.conteudos'); + final responses = []; + for (final url in urls) { + final response = await NetworkRouter.getWithCookies( + url, + { + 'pv_ocorrencia_id': occurId.toString(), + }, + session, + ); + responses.add(response); + } + return parseFilesMultipleRequests(responses); + } + Future> fetchCourseUnitClasses( Session session, int occurrId, diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 6ef395a2f..5409be874 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -1,7 +1,40 @@ +import 'dart:convert'; + import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; + +Future> parseFilesMultipleRequests( + List responses, +) async { + var files = []; + for (final response in responses) { + files += await parseFiles(response); + } + return files; +} + +Future> parseFiles(http.Response response) async { + final files = []; + + final json = jsonDecode(response.body) as List; + if (json.isEmpty) return []; + for (var item in json) { + item = item as Map; + print(item); + for (final file in item['ficheiros'] as Iterable) { + final fileName = file['nome'] as String; + final fileCode = file['codigo']; + final courseUnitFile = CourseUnitFile(fileName, + "https://sigarra.up.pt/feup/pt/conteudos_service.conteudos_cont?pct_id=$fileCode"); + files.add(courseUnitFile); + } + } + print(files); + return files; +} Future parseCourseUnitSheet(http.Response response) async { final document = parse(response.body); diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 2a98c4853..0f116fb2f 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -24,6 +24,8 @@ Future> parseSchedule(http.Response response) async { final json = jsonDecode(response.body) as Map; + print(json); + final schedule = json['horario'] as List; for (var lecture in schedule) { lecture = lecture as Map; diff --git a/uni/lib/model/entities/course_units/course_unit_file.dart b/uni/lib/model/entities/course_units/course_unit_file.dart new file mode 100644 index 000000000..c6b64b160 --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_file.dart @@ -0,0 +1,8 @@ +class CourseUnitFile { + CourseUnitFile( + this.name, + this.url, + ); + String name; + String url; +} 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 1f1e76a0b..67d55e28d 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -4,6 +4,7 @@ import 'package:logger/logger.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; @@ -15,6 +16,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { : super(dependsOnSession: true, cacheDuration: null, initialize: false); final Map _courseUnitsSheets = {}; final Map> _courseUnitsClasses = {}; + final Map> _courseUnitsFiles = {}; UnmodifiableMapView get courseUnitsSheets => UnmodifiableMapView(_courseUnitsSheets); @@ -22,6 +24,9 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { UnmodifiableMapView> get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); + UnmodifiableMapView> get courseUnitsFiles => + UnmodifiableMapView(_courseUnitsFiles); + Future fetchCourseUnitSheet( CourseUnit courseUnit, Session session, @@ -55,6 +60,22 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { updateStatus(RequestStatus.successful); } + Future fetchCourseUnitFiles( + CourseUnit courseUnit, + Session session, + ) async { + updateStatus(RequestStatus.busy); + try { + _courseUnitsFiles[courseUnit] = await CourseUnitsInfoFetcher() + .fetchCourseUnitFiles(session, courseUnit.occurrId); + } catch (e) { + updateStatus(RequestStatus.failed); + Logger().e('Failed to get course unit files for ${courseUnit.name}: $e'); + return; + } + updateStatus(RequestStatus.successful); + } + @override Future loadFromRemote(Session session, Profile profile) async { // Course units info is loaded on demand by its detail page 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 7c0d61596..b142ee4b9 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -7,6 +7,7 @@ import 'package:uni/view/common_widgets/page_title.dart'; import 'package:uni/view/common_widgets/pages_layouts/secondary/secondary.dart'; import 'package:uni/view/common_widgets/request_dependent_widget_builder.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_classes.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_files.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_sheet.dart'; import 'package:uni/view/lazy_consumer.dart'; @@ -36,6 +37,15 @@ class CourseUnitDetailPageViewState ); } + final courseUnitFiles = + courseUnitsProvider.courseUnitsFiles[widget.courseUnit]; + if (courseUnitFiles == null || force) { + await courseUnitsProvider.fetchCourseUnitFiles( + widget.courseUnit, + session, + ); + } + final courseUnitClasses = courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; if (courseUnitClasses == null || force) { @@ -59,7 +69,7 @@ class CourseUnitDetailPageViewState @override Widget getBody(BuildContext context) { return DefaultTabController( - length: 2, + length: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -68,7 +78,13 @@ class CourseUnitDetailPageViewState name: widget.courseUnit.name, ), const TabBar( - tabs: [Tab(text: 'Ficha'), Tab(text: 'Turmas')], + tabs: [ + Tab(text: 'Ficha'), + Tab(text: 'Turmas'), + Tab( + text: 'Ficheiros', + ) + ], ), Expanded( child: Padding( @@ -77,6 +93,7 @@ class CourseUnitDetailPageViewState children: [ _courseUnitSheetView(context), _courseUnitClassesView(context), + _courseUnitFilesView(context), ], ), ), @@ -110,6 +127,30 @@ class CourseUnitDetailPageViewState ); } + Widget _courseUnitFilesView(BuildContext context) { + return LazyConsumer( + builder: (context, courseUnitsInfoProvider) { + return RequestDependentWidgetBuilder( + onNullContent: const Center( + child: Text( + 'Não existem informações para apresentar', + textAlign: TextAlign.center, + ), + ), + status: courseUnitsInfoProvider.status, + builder: () => CourseUnitFilesView( + courseUnitsInfoProvider.courseUnitsFiles[widget.courseUnit]!, + ), + hasContentPredicate: + courseUnitsInfoProvider.courseUnitsFiles[widget.courseUnit] != + null && + courseUnitsInfoProvider + .courseUnitsFiles[widget.courseUnit]!.isNotEmpty, + ); + }, + ); + } + Widget _courseUnitClassesView(BuildContext context) { return LazyConsumer( builder: (context, courseUnitsInfoProvider) { diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart new file mode 100644 index 000000000..ed076aecd --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CourseUnitFilesView extends StatelessWidget { + const CourseUnitFilesView(this.files, {Key? key}) : super(key: key); + final List files; + + @override + Widget build(BuildContext context) { + final cards = files.map((file) => _buildCard(file)).toList(); + + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView(children: cards), + ); + } + + CourseUnitInfoCard _buildCard(CourseUnitFile file) { + return CourseUnitInfoCard( + file.name, + GestureDetector( + onTap: () { + _launchURL(file.url); + }, + child: Text('Download'), + ), + ); + } + + Future _launchURL(String url) async { + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } else { + // Handle the case when the URL cannot be launched + // For example, show an error message + } + } +} From cb4cc3c2f277c2edeaa9e676b88348d23bc8450e Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sun, 24 Sep 2023 02:51:04 +0100 Subject: [PATCH 02/21] Files page redesign --- .../course_units_info_fetcher.dart | 8 ++-- .../parsers/parser_course_unit_info.dart | 33 ++++++++------ .../controller/parsers/parser_schedule.dart | 2 - .../lazy/course_units_info_provider.dart | 7 +-- .../widgets/course_unit_files.dart | 29 +++++-------- .../widgets/course_unit_files_row.dart | 43 +++++++++++++++++++ 6 files changed, 82 insertions(+), 40 deletions(-) create mode 100644 uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart 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 0dd3caa3b..17425f23d 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,12 +1,12 @@ import 'package:html/parser.dart'; +import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_course_unit_info.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/model/entities/course_units/course_unit_file.dart'; -import 'package:http/http.dart'; class CourseUnitsInfoFetcher implements SessionDependantFetcher { @override @@ -28,7 +28,7 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseCourseUnitSheet(response); } - Future> fetchCourseUnitFiles( + Future>>> fetchCourseUnitFiles( Session session, int occurId, ) async { @@ -45,7 +45,7 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { ); responses.add(response); } - return parseFilesMultipleRequests(responses); + return parseFilesMultipleRequests(responses, session); } Future> fetchCourseUnitClasses( diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 5409be874..3d83637b3 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -3,37 +3,46 @@ import 'dart:convert'; import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/model/entities/course_units/course_unit_class.dart'; -import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; +import 'package:uni/model/entities/session.dart'; -Future> parseFilesMultipleRequests( +Future>>> parseFilesMultipleRequests( List responses, + Session session, ) async { - var files = []; + final files = >>[]; for (final response in responses) { - files += await parseFiles(response); + files.add(await parseFiles(response, session)); } return files; } -Future> parseFiles(http.Response response) async { - final files = []; +Future>> parseFiles( + http.Response response, + Session session, +) async { + final folders = >{}; final json = jsonDecode(response.body) as List; - if (json.isEmpty) return []; + + if (json.isEmpty) return {}; + for (var item in json) { item = item as Map; - print(item); + final files = []; for (final file in item['ficheiros'] as Iterable) { final fileName = file['nome'] as String; final fileCode = file['codigo']; - final courseUnitFile = CourseUnitFile(fileName, - "https://sigarra.up.pt/feup/pt/conteudos_service.conteudos_cont?pct_id=$fileCode"); + final courseUnitFile = CourseUnitFile( + fileName, + 'https://sigarra.up.pt/feup/pt/conteudos_service.conteudos_cont?pct_id=$fileCode', + ); files.add(courseUnitFile); } + folders[item['nome'] as String] = files; } - print(files); - return files; + return folders; } Future parseCourseUnitSheet(http.Response response) async { diff --git a/uni/lib/controller/parsers/parser_schedule.dart b/uni/lib/controller/parsers/parser_schedule.dart index 0f116fb2f..2a98c4853 100644 --- a/uni/lib/controller/parsers/parser_schedule.dart +++ b/uni/lib/controller/parsers/parser_schedule.dart @@ -24,8 +24,6 @@ Future> parseSchedule(http.Response response) async { final json = jsonDecode(response.body) as Map; - print(json); - final schedule = json['horario'] as List; for (var lecture in schedule) { lecture = lecture as Map; 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 67d55e28d..9a5bbce57 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -16,7 +16,8 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { : super(dependsOnSession: true, cacheDuration: null, initialize: false); final Map _courseUnitsSheets = {}; final Map> _courseUnitsClasses = {}; - final Map> _courseUnitsFiles = {}; + final Map>>> + _courseUnitsFiles = {}; UnmodifiableMapView get courseUnitsSheets => UnmodifiableMapView(_courseUnitsSheets); @@ -24,8 +25,8 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { UnmodifiableMapView> get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); - UnmodifiableMapView> get courseUnitsFiles => - UnmodifiableMapView(_courseUnitsFiles); + UnmodifiableMapView>>> + get courseUnitsFiles => UnmodifiableMapView(_courseUnitsFiles); Future fetchCourseUnitSheet( CourseUnit courseUnit, diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart index ed076aecd..88ec0e176 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/view/course_unit_info/widgets/course_unit_files_row.dart'; class CourseUnitFilesView extends StatelessWidget { const CourseUnitFilesView(this.files, {Key? key}) : super(key: key); - final List files; + final List>> files; @override Widget build(BuildContext context) { - final cards = files.map((file) => _buildCard(file)).toList(); + final cards = files + .expand((file) => + file.entries.map((item) => _buildCard(item.key, item.value))) + .toList(); return Container( padding: const EdgeInsets.only(left: 10, right: 10), @@ -17,24 +20,12 @@ class CourseUnitFilesView extends StatelessWidget { ); } - CourseUnitInfoCard _buildCard(CourseUnitFile file) { + CourseUnitInfoCard _buildCard(String folder, List files) { return CourseUnitInfoCard( - file.name, - GestureDetector( - onTap: () { - _launchURL(file.url); - }, - child: Text('Download'), + folder, + Column( + children: files.map((file) => CourseUnitFilesRow(file)).toList(), ), ); } - - Future _launchURL(String url) async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } else { - // Handle the case when the URL cannot be launched - // For example, show an error message - } - } } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart new file mode 100644 index 000000000..d0a41accd --- /dev/null +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CourseUnitFilesRow extends StatelessWidget { + const CourseUnitFilesRow(this.file, {Key? key}) : super(key: key); + + final CourseUnitFile file; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Text( + file.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), + IconButton( + icon: Icon(Icons.download), + onPressed: () => _launchURL(file.url), + ), + ], + ), + ); + } + + Future _launchURL(String url) async { + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } else { + // Handle the case when the URL cannot be launched + // For example, show an error message + } + } +} From 3e057de3b2e25f28e43f37aae65fb2a35006fafc Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 28 Sep 2023 13:06:55 +0100 Subject: [PATCH 03/21] Pdf icon and open file support --- .../course_units_info_fetcher.dart | 15 ++++++++++ .../parsers/parser_course_unit_info.dart | 5 +++- .../course_units/course_unit_file.dart | 4 +-- .../widgets/course_unit_files.dart | 5 ++-- .../widgets/course_unit_files_row.dart | 29 ++++++++++++++----- uni/pubspec.yaml | 1 + 6 files changed, 46 insertions(+), 13 deletions(-) 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 17425f23d..585ea6dc7 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 @@ -48,6 +48,21 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseFilesMultipleRequests(responses, session); } + Future> downloadFile( + Session session, + dynamic fileCode, + ) async { + final url = '${getEndpoints(session)[0]}conteudos_service.conteudos_cont'; + final response = await NetworkRouter.getWithCookies( + url, + { + 'pct_id': fileCode.toString(), + }, + session, + ); + return response.bodyBytes; + } + Future> fetchCourseUnitClasses( Session session, int occurrId, diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 3d83637b3..49b31f2cb 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -6,6 +6,7 @@ import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; +import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; Future>>> parseFilesMultipleRequests( List responses, @@ -34,9 +35,11 @@ Future>> parseFiles( for (final file in item['ficheiros'] as Iterable) { final fileName = file['nome'] as String; final fileCode = file['codigo']; + final bodyBytes = + await CourseUnitsInfoFetcher().downloadFile(session, fileCode); final courseUnitFile = CourseUnitFile( fileName, - 'https://sigarra.up.pt/feup/pt/conteudos_service.conteudos_cont?pct_id=$fileCode', + bodyBytes, ); files.add(courseUnitFile); } diff --git a/uni/lib/model/entities/course_units/course_unit_file.dart b/uni/lib/model/entities/course_units/course_unit_file.dart index c6b64b160..7375fef1c 100644 --- a/uni/lib/model/entities/course_units/course_unit_file.dart +++ b/uni/lib/model/entities/course_units/course_unit_file.dart @@ -1,8 +1,8 @@ class CourseUnitFile { CourseUnitFile( this.name, - this.url, + this.bodyBytes, ); String name; - String url; + List bodyBytes; } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart index 88ec0e176..23badd351 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -10,8 +10,9 @@ class CourseUnitFilesView extends StatelessWidget { @override Widget build(BuildContext context) { final cards = files - .expand((file) => - file.entries.map((item) => _buildCard(item.key, item.value))) + .expand((file) => file.entries + .where((item) => item.value.isNotEmpty) + .map((item) => _buildCard(item.key, item.value))) .toList(); return Container( diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index d0a41accd..19c4e0b78 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:open_file_plus/open_file_plus.dart'; class CourseUnitFilesRow extends StatelessWidget { const CourseUnitFilesRow(this.file, {Key? key}) : super(key: key); @@ -13,6 +16,9 @@ class CourseUnitFilesRow extends StatelessWidget { padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ + const SizedBox(width: 8), + const Icon(Icons.picture_as_pdf), + const SizedBox(width: 1), Expanded( child: Container( padding: const EdgeInsets.only(left: 10), @@ -25,19 +31,26 @@ class CourseUnitFilesRow extends StatelessWidget { ), IconButton( icon: Icon(Icons.download), - onPressed: () => _launchURL(file.url), + onPressed: () => openFile(file), ), ], ), ); } - Future _launchURL(String url) async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } else { - // Handle the case when the URL cannot be launched - // For example, show an error message + Future openFile(CourseUnitFile course_unit_file) async { + final response = course_unit_file.bodyBytes; + + final String fileName = course_unit_file.name + '.pdf'; + + final downloadsDir = await getDownloadsDirectory(); + final downloadPath = '${downloadsDir!.path}/$fileName'; + + final file = File(downloadPath); + if (!await file.exists()) { + await file.create(); } + await file.writeAsBytes(response); + await OpenFile.open(file.path.toString()); } } diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index fdbccd0c1..f3bbb9349 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: latlong2: ^0.8.1 logger: ^1.1.0 material_design_icons_flutter: ^7.0.7296 + open_file_plus: ^3.4.1 path: ^1.8.0 path_provider: ^2.0.0 percent_indicator: ^4.2.2 From b3bbc32f92535f3d314779a5d3e2130bc5aa99b2 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sun, 1 Oct 2023 21:01:31 +0100 Subject: [PATCH 04/21] Lint fix --- .../parsers/parser_course_unit_info.dart | 2 +- .../course_unit_info/course_unit_info.dart | 2 +- .../widgets/course_unit_files.dart | 12 +++++----- .../widgets/course_unit_files_row.dart | 22 +++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 49b31f2cb..6c39f0cba 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'package:html/parser.dart'; import 'package:http/http.dart' as http; +import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; -import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; Future>>> parseFilesMultipleRequests( List responses, 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 c2b2d7a84..1aa43b8cc 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -82,7 +82,7 @@ class CourseUnitDetailPageViewState tabs: [ Tab(text: S.of(context).course_info), Tab(text: S.of(context).course_class), - Tab( + const Tab( text: 'Ficheiros', ) ], diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart index 23badd351..8878174fe 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -4,15 +4,17 @@ import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_files_row.dart'; class CourseUnitFilesView extends StatelessWidget { - const CourseUnitFilesView(this.files, {Key? key}) : super(key: key); + const CourseUnitFilesView(this.files, {super.key}); final List>> files; @override Widget build(BuildContext context) { final cards = files - .expand((file) => file.entries - .where((item) => item.value.isNotEmpty) - .map((item) => _buildCard(item.key, item.value))) + .expand( + (file) => file.entries + .where((item) => item.value.isNotEmpty) + .map((item) => _buildCard(item.key, item.value)), + ) .toList(); return Container( @@ -25,7 +27,7 @@ class CourseUnitFilesView extends StatelessWidget { return CourseUnitInfoCard( folder, Column( - children: files.map((file) => CourseUnitFilesRow(file)).toList(), + children: files.map(CourseUnitFilesRow.new).toList(), ), ); } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 19c4e0b78..23036aea1 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:open_file_plus/open_file_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; -import 'package:open_file_plus/open_file_plus.dart'; class CourseUnitFilesRow extends StatelessWidget { - const CourseUnitFilesRow(this.file, {Key? key}) : super(key: key); + const CourseUnitFilesRow(this.file, {super.key}); final CourseUnitFile file; @@ -30,7 +30,7 @@ class CourseUnitFilesRow extends StatelessWidget { ), ), IconButton( - icon: Icon(Icons.download), + icon: const Icon(Icons.download), onPressed: () => openFile(file), ), ], @@ -38,19 +38,19 @@ class CourseUnitFilesRow extends StatelessWidget { ); } - Future openFile(CourseUnitFile course_unit_file) async { - final response = course_unit_file.bodyBytes; + Future openFile(CourseUnitFile unitFile) async { + final response = unitFile.bodyBytes; - final String fileName = course_unit_file.name + '.pdf'; + final fileName = '${unitFile.name}.pdf'; - final downloadsDir = await getDownloadsDirectory(); - final downloadPath = '${downloadsDir!.path}/$fileName'; + final downloadDir = (await getTemporaryDirectory()).path; + final downloadPath = '$downloadDir$fileName'; final file = File(downloadPath); - if (!await file.exists()) { - await file.create(); + if (!file.existsSync()) { + file.createSync(); } await file.writeAsBytes(response); - await OpenFile.open(file.path.toString()); + await OpenFile.open(file.path); } } From 3fccae6377aa382f684a31b2cb4dfbcb84d494af Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 5 Oct 2023 19:17:30 +0100 Subject: [PATCH 05/21] Opening file fix and dependency update --- .../course_unit_info/widgets/course_unit_files_row.dart | 2 +- uni/pubspec.lock | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 23036aea1..b39ae76c3 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -44,7 +44,7 @@ class CourseUnitFilesRow extends StatelessWidget { final fileName = '${unitFile.name}.pdf'; final downloadDir = (await getTemporaryDirectory()).path; - final downloadPath = '$downloadDir$fileName'; + final downloadPath = '$downloadDir/$fileName'; final file = File(downloadPath); if (!file.existsSync()) { diff --git a/uni/pubspec.lock b/uni/pubspec.lock index c2e1870fb..2c732cb72 100644 --- a/uni/pubspec.lock +++ b/uni/pubspec.lock @@ -686,6 +686,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + open_file_plus: + dependency: "direct main" + description: + name: open_file_plus + sha256: f087e32722ffe4bac71925e7a1a9848a1008fd789e47c6628da3ed7845922227 + url: "https://pub.dev" + source: hosted + version: "3.4.1" package_config: dependency: transitive description: From 8ec07b6c0dbbe1174307698fd04afea331afc762 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 5 Oct 2023 20:23:48 +0100 Subject: [PATCH 06/21] Local storage integration --- .../course_units_info_fetcher.dart | 13 ++------- .../local_storage/file_offline_storage.dart | 4 ++- .../parsers/parser_course_unit_info.dart | 12 ++++---- .../course_units/course_unit_file.dart | 6 ++-- .../widgets/course_unit_files_row.dart | 29 +++++++++---------- 5 files changed, 28 insertions(+), 36 deletions(-) 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 585ea6dc7..bd0278913 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 @@ -48,19 +48,10 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseFilesMultipleRequests(responses, session); } - Future> downloadFile( + Future getDownloadLink( Session session, - dynamic fileCode, ) async { - final url = '${getEndpoints(session)[0]}conteudos_service.conteudos_cont'; - final response = await NetworkRouter.getWithCookies( - url, - { - 'pct_id': fileCode.toString(), - }, - session, - ); - return response.bodyBytes; + return '${getEndpoints(session)[0]}conteudos_service.conteudos_cont'; } Future> fetchCourseUnitClasses( diff --git a/uni/lib/controller/local_storage/file_offline_storage.dart b/uni/lib/controller/local_storage/file_offline_storage.dart index 9580def16..afad71b3a 100644 --- a/uni/lib/controller/local_storage/file_offline_storage.dart +++ b/uni/lib/controller/local_storage/file_offline_storage.dart @@ -67,9 +67,11 @@ Future _downloadAndSaveFile( Session? session, Map? headers, ) async { + final header = headers ?? {}; + final response = session == null ? await http.get(url.toUri(), headers: headers) - : await NetworkRouter.getWithCookies(url, {}, session); + : await NetworkRouter.getWithCookies(url, header, session); if (response.statusCode == 200) { return File(filePath).writeAsBytes(response.bodyBytes); diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 6c39f0cba..7f6892ab0 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -32,14 +32,14 @@ Future>> parseFiles( for (var item in json) { item = item as Map; final files = []; - for (final file in item['ficheiros'] as Iterable) { - final fileName = file['nome'] as String; + for (final file in item['ficheiros'] as List) { + final fileName = file['nome']; final fileCode = file['codigo']; - final bodyBytes = - await CourseUnitsInfoFetcher().downloadFile(session, fileCode); + final url = await CourseUnitsInfoFetcher().getDownloadLink(session); final courseUnitFile = CourseUnitFile( - fileName, - bodyBytes, + fileName.toString(), + url, + fileCode.toString(), ); files.add(courseUnitFile); } diff --git a/uni/lib/model/entities/course_units/course_unit_file.dart b/uni/lib/model/entities/course_units/course_unit_file.dart index 7375fef1c..e7076df1d 100644 --- a/uni/lib/model/entities/course_units/course_unit_file.dart +++ b/uni/lib/model/entities/course_units/course_unit_file.dart @@ -1,8 +1,10 @@ class CourseUnitFile { CourseUnitFile( this.name, - this.bodyBytes, + this.url, + this.fileCode, ); + String fileCode; String name; - List bodyBytes; + String url; } diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index b39ae76c3..8fb8e05b5 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -1,9 +1,9 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:open_file_plus/open_file_plus.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/model/providers/startup/session_provider.dart'; class CourseUnitFilesRow extends StatelessWidget { const CourseUnitFilesRow(this.file, {super.key}); @@ -31,26 +31,23 @@ class CourseUnitFilesRow extends StatelessWidget { ), IconButton( icon: const Icon(Icons.download), - onPressed: () => openFile(file), + onPressed: () => openFile(context, file), ), ], ), ); } - Future openFile(CourseUnitFile unitFile) async { - final response = unitFile.bodyBytes; - - final fileName = '${unitFile.name}.pdf'; + Future openFile(BuildContext context, CourseUnitFile unitFile) async { + final session = context.read().session; - final downloadDir = (await getTemporaryDirectory()).path; - final downloadPath = '$downloadDir/$fileName'; + final result = await loadFileFromStorageOrRetrieveNew( + '${unitFile.name}.pdf', + unitFile.url, + session, + headers: {'pct_id': unitFile.fileCode}, + ); - final file = File(downloadPath); - if (!file.existsSync()) { - file.createSync(); - } - await file.writeAsBytes(response); - await OpenFile.open(file.path); + await OpenFile.open(result!.path); } } From 22dba728660b01bfe674827c67d50b1f75e50d99 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 5 Oct 2023 20:39:46 +0100 Subject: [PATCH 07/21] Card redesign --- .../widgets/course_unit_files_row.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 8fb8e05b5..095c6aff9 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -13,26 +13,25 @@ class CourseUnitFilesRow extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.only(bottom: 20), child: Row( children: [ const SizedBox(width: 8), const Icon(Icons.picture_as_pdf), const SizedBox(width: 1), Expanded( - child: Container( - padding: const EdgeInsets.only(left: 10), - child: Text( - file.name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, + child: InkWell( + onTap: () => openFile(context, file), + child: Container( + padding: const EdgeInsets.only(left: 10), + child: Text( + file.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), ), ), ), - IconButton( - icon: const Icon(Icons.download), - onPressed: () => openFile(context, file), - ), ], ), ); From 4613cebc34cc571c941b284b662022ac307bce4a Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 13 Oct 2023 10:09:00 +0100 Subject: [PATCH 08/21] File name changes --- uni/lib/controller/parsers/parser_course_unit_info.dart | 2 +- .../view/course_unit_info/widgets/course_unit_files_row.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 7f6892ab0..818a457b5 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -33,7 +33,7 @@ Future>> parseFiles( item = item as Map; final files = []; for (final file in item['ficheiros'] as List) { - final fileName = file['nome']; + final fileName = file['nome'] + '_' + file['data_actualizacao']; final fileCode = file['codigo']; final url = await CourseUnitsInfoFetcher().getDownloadLink(session); final courseUnitFile = CourseUnitFile( diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 095c6aff9..913af7ade 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -25,7 +25,7 @@ class CourseUnitFilesRow extends StatelessWidget { child: Container( padding: const EdgeInsets.only(left: 10), child: Text( - file.name, + file.name.substring(0, file.name.indexOf('_')), overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, ), From c4b8d9c26eb4a39f854451986f167f19c8023231 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 20 Oct 2023 14:55:33 +0100 Subject: [PATCH 09/21] Cache clean up flow --- uni/lib/controller/cleanup.dart | 37 ++++++++++++++++ .../local_storage/app_shared_preferences.dart | 13 ++++++ .../local_storage/file_offline_storage.dart | 2 +- uni/lib/main.dart | 3 ++ uni/pubspec.lock | 42 +++++++++++-------- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/uni/lib/controller/cleanup.dart b/uni/lib/controller/cleanup.dart index 11bea65d2..079845e30 100644 --- a/uni/lib/controller/cleanup.dart +++ b/uni/lib/controller/cleanup.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; @@ -41,3 +42,39 @@ Future cleanupStoredData(BuildContext context) async { directory.deleteSync(recursive: true); } } + +Future cleanupCachedFiles() async { + final lastCleanupDate = await AppSharedPreferences.getLastCleanUpDate(); + final daysSinceLastCleanup = + DateTime.now().difference(lastCleanupDate).inDays; + + if (daysSinceLastCleanup < 14) { + return; + } + + final cacheManager = DefaultCacheManager(); + final cacheDirectory = await getApplicationDocumentsDirectory(); + final treshold = DateTime.now().subtract(const Duration(days: 30)); + + final directories = cacheDirectory.listSync(followLinks: false); + for (final directory in directories) { + if (directory is Directory) { + final files = directory.listSync(recursive: true, followLinks: false); + + final oldFiles = files.where((file) { + try { + final fileDate = File(file.path).lastModifiedSync(); + return fileDate.isBefore(treshold); + } catch (e) { + return false; + } + }); + + for (final file in oldFiles) { + await cacheManager.removeFile(file.path); + } + } + } + + await AppSharedPreferences.setLastCleanUpDate(DateTime.now()); +} diff --git a/uni/lib/controller/local_storage/app_shared_preferences.dart b/uni/lib/controller/local_storage/app_shared_preferences.dart index faca04476..748067d84 100644 --- a/uni/lib/controller/local_storage/app_shared_preferences.dart +++ b/uni/lib/controller/local_storage/app_shared_preferences.dart @@ -28,6 +28,7 @@ class AppSharedPreferences { 'tuition_notification_toogle'; static const String themeMode = 'theme_mode'; static const String locale = 'app_locale'; + static const String lastCacheCleanUpDate = 'last_clean'; static const String favoriteCards = 'favorite_cards'; static final List defaultFavoriteCards = [ FavoriteWidgetType.schedule, @@ -136,6 +137,18 @@ class AppSharedPreferences { ); } + static Future setLastCleanUpDate(DateTime date) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(lastCacheCleanUpDate, date.toString()); + } + + static Future getLastCleanUpDate() async { + final prefs = await SharedPreferences.getInstance(); + final date = + prefs.getString(lastCacheCleanUpDate) ?? DateTime.now().toString(); + return DateTime.parse(date); + } + /// Deletes the user's student number and password. static Future removePersistentUserInfo() async { final prefs = await SharedPreferences.getInstance(); diff --git a/uni/lib/controller/local_storage/file_offline_storage.dart b/uni/lib/controller/local_storage/file_offline_storage.dart index afad71b3a..768661709 100644 --- a/uni/lib/controller/local_storage/file_offline_storage.dart +++ b/uni/lib/controller/local_storage/file_offline_storage.dart @@ -11,7 +11,7 @@ import 'package:uni/model/entities/session.dart'; /// The offline image storage location on the device. Future get _localPath async { - final directory = await getTemporaryDirectory(); + final directory = await getApplicationDocumentsDirectory(); return directory.path; } diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 01fac7edd..0d9d36e45 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -9,6 +9,7 @@ import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/background_workers/background_callback.dart'; +import 'package:uni/controller/cleanup.dart'; import 'package:uni/controller/load_static/terms_and_conditions.dart'; import 'package:uni/controller/local_storage/app_shared_preferences.dart'; import 'package:uni/generated/l10n.dart'; @@ -79,6 +80,8 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); + unawaited(cleanupCachedFiles()); + // Initialize WorkManager for background tasks await Workmanager().initialize( workerStartCallback, diff --git a/uni/pubspec.lock b/uni/pubspec.lock index 8da48a9fa..394e8fbb7 100644 --- a/uni/pubspec.lock +++ b/uni/pubspec.lock @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" connectivity_plus: dependency: "direct main" description: @@ -521,10 +521,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -593,18 +593,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" material_design_icons_flutter: dependency: "direct main" description: @@ -1015,10 +1015,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sqflite: dependency: "direct main" description: @@ -1087,26 +1087,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" url: "https://pub.dev" source: hosted - version: "1.24.1" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" timelines: dependency: "direct main" description: @@ -1283,6 +1283,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1348,5 +1356,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.6" From c8608ad6a144a372a52ef8caa69e6e032c117668 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 1 Dec 2023 13:10:53 +0000 Subject: [PATCH 10/21] Implementing text animation and improving cleaning flow --- uni/lib/controller/cleanup.dart | 8 +-- .../parsers/parser_course_unit_info.dart | 22 +++++--- .../course_unit_info/course_unit_info.dart | 2 +- .../widgets/course_unit_files_row.dart | 56 ++++++++++++++++--- uni/pubspec.lock | 11 ++-- uni/pubspec.yaml | 8 ++- 6 files changed, 79 insertions(+), 28 deletions(-) diff --git a/uni/lib/controller/cleanup.dart b/uni/lib/controller/cleanup.dart index 079845e30..955b88ffa 100644 --- a/uni/lib/controller/cleanup.dart +++ b/uni/lib/controller/cleanup.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uni/controller/local_storage/app_bus_stop_database.dart'; @@ -52,11 +51,10 @@ Future cleanupCachedFiles() async { return; } - final cacheManager = DefaultCacheManager(); - final cacheDirectory = await getApplicationDocumentsDirectory(); + final toCleanDirectory = await getApplicationDocumentsDirectory(); final treshold = DateTime.now().subtract(const Duration(days: 30)); + final directories = toCleanDirectory.listSync(followLinks: false); - final directories = cacheDirectory.listSync(followLinks: false); for (final directory in directories) { if (directory is Directory) { final files = directory.listSync(recursive: true, followLinks: false); @@ -71,7 +69,7 @@ Future cleanupCachedFiles() async { }); for (final file in oldFiles) { - await cacheManager.removeFile(file.path); + await File(file.path).delete(); } } } diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 818a457b5..98fbda163 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -33,15 +33,19 @@ Future>> parseFiles( item = item as Map; final files = []; for (final file in item['ficheiros'] as List) { - final fileName = file['nome'] + '_' + file['data_actualizacao']; - final fileCode = file['codigo']; - final url = await CourseUnitsInfoFetcher().getDownloadLink(session); - final courseUnitFile = CourseUnitFile( - fileName.toString(), - url, - fileCode.toString(), - ); - files.add(courseUnitFile); + if(file is Map){ + final fileName = file['nome'].toString(); + final fileDate = file['data_actualizacao'].toString(); + final fileCode = file['codigo'].toString(); + final url = await CourseUnitsInfoFetcher().getDownloadLink(session); + final courseUnitFile = CourseUnitFile( + '${fileName}_$fileDate', + url, + fileCode, + ); + files.add(courseUnitFile); + } + } folders[item['nome'] as String] = files; } 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 a77270ff8..0602c6b88 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -83,7 +83,7 @@ class CourseUnitDetailPageViewState Tab(text: S.of(context).course_class), const Tab( text: 'Ficheiros', - ) + ), ], ), Expanded( diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 913af7ade..5426096fb 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:open_file_plus/open_file_plus.dart'; import 'package:provider/provider.dart'; @@ -5,11 +7,36 @@ import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; -class CourseUnitFilesRow extends StatelessWidget { +class CourseUnitFilesRow extends StatefulWidget { const CourseUnitFilesRow(this.file, {super.key}); final CourseUnitFile file; + @override + State createState() { + return CourseUnitFilesRowState(); + } +} + +class CourseUnitFilesRowState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( @@ -20,14 +47,28 @@ class CourseUnitFilesRow extends StatelessWidget { const Icon(Icons.picture_as_pdf), const SizedBox(width: 1), Expanded( - child: InkWell( - onTap: () => openFile(context, file), + child: GestureDetector( + onTap: () { + _controller + ..reset() + ..repeat(reverse: true); + openFile(context, widget.file); + }, child: Container( padding: const EdgeInsets.only(left: 10), - child: Text( - file.name.substring(0, file.name.indexOf('_')), - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Opacity( + opacity: 1 - 0.5 * sin(_controller.value * pi), + child: Text( + widget.file.name + .substring(0, widget.file.name.indexOf('_')), + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + }, ), ), ), @@ -48,5 +89,6 @@ class CourseUnitFilesRow extends StatelessWidget { ); await OpenFile.open(result!.path); + _controller.reset(); } } diff --git a/uni/pubspec.lock b/uni/pubspec.lock index 3f8fe9ebf..17866fc25 100644 --- a/uni/pubspec.lock +++ b/uni/pubspec.lock @@ -351,7 +351,7 @@ packages: source: sdk version: "0.0.0" flutter_cache_manager: - dependency: transitive + dependency: "direct main" description: name: flutter_cache_manager sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" @@ -681,10 +681,11 @@ packages: open_file_plus: dependency: "direct main" description: - name: open_file_plus - sha256: f087e32722ffe4bac71925e7a1a9848a1008fd789e47c6628da3ed7845922227 - url: "https://pub.dev" - source: hosted + path: "." + ref: "3c32191" + resolved-ref: "3c321911c54388d1316e34d4f999776281398fc2" + url: "https://github.com/joutvhu/open_file_plus.git" + source: git version: "3.4.1" package_config: dependency: transitive diff --git a/uni/pubspec.yaml b/uni/pubspec.yaml index d1177cd51..3bf89c0b7 100644 --- a/uni/pubspec.yaml +++ b/uni/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: expansion_tile_card: ^3.0.0 flutter: sdk: flutter + flutter_cache_manager: ^3.3.1 flutter_dotenv: ^5.0.2 flutter_local_notifications: ^15.1.0+1 flutter_localizations: @@ -42,7 +43,10 @@ dependencies: latlong2: ^0.9.0 logger: ^2.0.2+1 material_design_icons_flutter: ^7.0.7296 - open_file_plus: ^3.4.1 + open_file_plus: + git: + url: https://github.com/joutvhu/open_file_plus.git + ref: "3c32191" path: ^1.8.0 path_provider: ^2.0.0 percent_indicator: ^4.2.2 @@ -68,8 +72,10 @@ dev_dependencies: git: url: https://github.com/dart-lang/mockito.git ref: "e54a006" + test: any very_good_analysis: ^5.1.0 + flutter: generate: true From 32325d031ef49695caf056f75fc67faccf2f8d82 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 1 Dec 2023 13:19:08 +0000 Subject: [PATCH 11/21] Fixing format --- .../parsers/parser_course_unit_info.dart | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 98fbda163..d93ff9e56 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -33,19 +33,18 @@ Future>> parseFiles( item = item as Map; final files = []; for (final file in item['ficheiros'] as List) { - if(file is Map){ - final fileName = file['nome'].toString(); - final fileDate = file['data_actualizacao'].toString(); - final fileCode = file['codigo'].toString(); - final url = await CourseUnitsInfoFetcher().getDownloadLink(session); - final courseUnitFile = CourseUnitFile( - '${fileName}_$fileDate', - url, - fileCode, - ); - files.add(courseUnitFile); + if (file is Map) { + final fileName = file['nome'].toString(); + final fileDate = file['data_actualizacao'].toString(); + final fileCode = file['codigo'].toString(); + final url = await CourseUnitsInfoFetcher().getDownloadLink(session); + final courseUnitFile = CourseUnitFile( + '${fileName}_$fileDate', + url, + fileCode, + ); + files.add(courseUnitFile); } - } folders[item['nome'] as String] = files; } From c8c97b4df3cdc6fe41ca594a9a38b906b5747a30 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 8 Dec 2023 14:45:50 +0000 Subject: [PATCH 12/21] Catching a timeout error while opening files --- .../widgets/course_unit_files_row.dart | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 5426096fb..8e875e109 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -5,6 +5,8 @@ import 'package:open_file_plus/open_file_plus.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/view/common_widgets/toast_message.dart'; +import 'package:uni/generated/l10n.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; class CourseUnitFilesRow extends StatefulWidget { @@ -81,14 +83,23 @@ class CourseUnitFilesRowState extends State Future openFile(BuildContext context, CourseUnitFile unitFile) async { final session = context.read().session; - final result = await loadFileFromStorageOrRetrieveNew( - '${unitFile.name}.pdf', - unitFile.url, - session, - headers: {'pct_id': unitFile.fileCode}, - ); + try { + final result = await loadFileFromStorageOrRetrieveNew( + '${unitFile.name}.pdf', + unitFile.url, + session, + headers: {'pct_id': unitFile.fileCode}, + ); - await OpenFile.open(result!.path); + await OpenFile.open(result!.path); + } catch (e) { + if (context.mounted) { + await ToastMessage.error( + context, + S.of(context).load_error, + ); + } + } _controller.reset(); } } From 5a2df5839eaa6bc7dece0627de102824008ccbfe Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 8 Dec 2023 14:51:46 +0000 Subject: [PATCH 13/21] Lint fix --- .../view/course_unit_info/widgets/course_unit_files_row.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 8e875e109..1f6c2581e 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -4,10 +4,10 @@ import 'package:flutter/material.dart'; import 'package:open_file_plus/open_file_plus.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; -import 'package:uni/model/entities/course_units/course_unit_file.dart'; -import 'package:uni/view/common_widgets/toast_message.dart'; import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/view/common_widgets/toast_message.dart'; class CourseUnitFilesRow extends StatefulWidget { const CourseUnitFilesRow(this.file, {super.key}); From ebbc3fe27cf8a9ce26cc36ac5ace7af8e7264b4a Mon Sep 17 00:00:00 2001 From: DGoiana Date: Thu, 4 Jan 2024 18:36:05 +0000 Subject: [PATCH 14/21] Translating and abstracting code --- .../course_units_info_fetcher.dart | 28 ++++++++----------- .../parsers/parser_course_unit_info.dart | 24 ++++------------ uni/lib/generated/intl/messages_en.dart | 1 + uni/lib/generated/intl/messages_pt_PT.dart | 1 + uni/lib/generated/l10n.dart | 10 +++++++ uni/lib/l10n/intl_en.arb | 2 ++ uni/lib/l10n/intl_pt_PT.arb | 2 ++ .../course_units/course_unit_directory.dart | 8 ++++++ .../lazy/course_units_info_provider.dart | 7 ++--- .../course_unit_info/course_unit_info.dart | 6 ++-- .../widgets/course_unit_files.dart | 25 ++++++++--------- .../view/schedule/widgets/schedule_slot.dart | 8 +++--- 12 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 uni/lib/model/entities/course_units/course_unit_directory.dart 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 bd0278913..1c54029e3 100644 --- a/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart +++ b/uni/lib/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart @@ -1,10 +1,9 @@ import 'package:html/parser.dart'; -import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/controller/parsers/parser_course_unit_info.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; -import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; @@ -28,24 +27,19 @@ class CourseUnitsInfoFetcher implements SessionDependantFetcher { return parseCourseUnitSheet(response); } - Future>>> fetchCourseUnitFiles( + Future> fetchCourseUnitFiles( Session session, int occurId, ) async { - final urls = - getEndpoints(session).map((url) => '${url}mob_ucurr_geral.conteudos'); - final responses = []; - for (final url in urls) { - final response = await NetworkRouter.getWithCookies( - url, - { - 'pv_ocorrencia_id': occurId.toString(), - }, - session, - ); - responses.add(response); - } - return parseFilesMultipleRequests(responses, session); + final url = '${getEndpoints(session)[0]}mob_ucurr_geral.conteudos'; + final response = await NetworkRouter.getWithCookies( + url, + { + 'pv_ocorrencia_id': occurId.toString(), + }, + session, + ); + return parseFiles(response, session); } Future getDownloadLink( diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index d93ff9e56..9bf3deac7 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -4,30 +4,18 @@ import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/session.dart'; -Future>>> parseFilesMultipleRequests( - List responses, - Session session, -) async { - final files = >>[]; - for (final response in responses) { - files.add(await parseFiles(response, session)); - } - return files; -} - -Future>> parseFiles( +Future> parseFiles( http.Response response, Session session, ) async { - final folders = >{}; - final json = jsonDecode(response.body) as List; - - if (json.isEmpty) return {}; + final dirs = []; + if (json.isEmpty) return []; for (var item in json) { item = item as Map; @@ -46,9 +34,9 @@ Future>> parseFiles( files.add(courseUnitFile); } } - folders[item['nome'] as String] = files; + dirs.add(CourseUnitFileDirectory(item['nome'].toString(), files)); } - return folders; + return dirs; } Future parseCourseUnitSheet(http.Response response) async { diff --git a/uni/lib/generated/intl/messages_en.dart b/uni/lib/generated/intl/messages_en.dart index dc2124c51..d04d78835 100644 --- a/uni/lib/generated/intl/messages_en.dart +++ b/uni/lib/generated/intl/messages_en.dart @@ -126,6 +126,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Deadline for next fee:"), "fee_notification": MessageLookupByLibrary.simpleMessage("Fee deadline"), + "files": MessageLookupByLibrary.simpleMessage("Files"), "first_year_registration": MessageLookupByLibrary.simpleMessage( "Year of first registration: "), "floor": MessageLookupByLibrary.simpleMessage("Floor"), diff --git a/uni/lib/generated/intl/messages_pt_PT.dart b/uni/lib/generated/intl/messages_pt_PT.dart index 3bba2a947..2b56f0c60 100644 --- a/uni/lib/generated/intl/messages_pt_PT.dart +++ b/uni/lib/generated/intl/messages_pt_PT.dart @@ -125,6 +125,7 @@ class MessageLookup extends MessageLookupByLibrary { "Data limite próxima prestação:"), "fee_notification": MessageLookupByLibrary.simpleMessage("Data limite de propina"), + "files": MessageLookupByLibrary.simpleMessage("Ficheiros"), "first_year_registration": MessageLookupByLibrary.simpleMessage("Ano da primeira inscrição: "), "floor": MessageLookupByLibrary.simpleMessage("Piso"), diff --git a/uni/lib/generated/l10n.dart b/uni/lib/generated/l10n.dart index e1c460e90..4fa6b95b5 100644 --- a/uni/lib/generated/l10n.dart +++ b/uni/lib/generated/l10n.dart @@ -1246,6 +1246,16 @@ class S { ); } + /// `Files` + String get files { + return Intl.message( + 'Files', + name: 'files', + desc: '', + args: [], + ); + } + /// `School Calendar` String get school_calendar { return Intl.message( diff --git a/uni/lib/l10n/intl_en.arb b/uni/lib/l10n/intl_en.arb index 41134b39f..37fe6211e 100644 --- a/uni/lib/l10n/intl_en.arb +++ b/uni/lib/l10n/intl_en.arb @@ -244,6 +244,8 @@ "@restaurant_main_page": {}, "room": "Room", "@room": {}, + "files": "Files", + "@files": {}, "school_calendar": "School Calendar", "@school_calendar": {}, "semester": "Semester", diff --git a/uni/lib/l10n/intl_pt_PT.arb b/uni/lib/l10n/intl_pt_PT.arb index 0529b7216..a773e58a1 100644 --- a/uni/lib/l10n/intl_pt_PT.arb +++ b/uni/lib/l10n/intl_pt_PT.arb @@ -244,6 +244,8 @@ "@restaurant_main_page": {}, "room": "Sala", "@room": {}, + "files": "Ficheiros", + "@files": {}, "school_calendar": "Calendário Escolar", "@school_calendar": {}, "semester": "Semestre", diff --git a/uni/lib/model/entities/course_units/course_unit_directory.dart b/uni/lib/model/entities/course_units/course_unit_directory.dart new file mode 100644 index 000000000..d14fa6b13 --- /dev/null +++ b/uni/lib/model/entities/course_units/course_unit_directory.dart @@ -0,0 +1,8 @@ +import 'package:uni/model/entities/course_units/course_unit_file.dart'; + +class CourseUnitFileDirectory { + CourseUnitFileDirectory(this.folderName, this.files); + + final String folderName; + final List files; +} diff --git a/uni/lib/model/providers/lazy/course_units_info_provider.dart b/uni/lib/model/providers/lazy/course_units_info_provider.dart index 77b205a26..41551d6be 100644 --- a/uni/lib/model/providers/lazy/course_units_info_provider.dart +++ b/uni/lib/model/providers/lazy/course_units_info_provider.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; -import 'package:uni/model/entities/course_units/course_unit_file.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; import 'package:uni/model/entities/course_units/course_unit_sheet.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/entities/session.dart'; @@ -14,8 +14,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { : super(dependsOnSession: true, cacheDuration: null, initialize: false); final Map _courseUnitsSheets = {}; final Map> _courseUnitsClasses = {}; - final Map>>> - _courseUnitsFiles = {}; + final Map> _courseUnitsFiles = {}; UnmodifiableMapView get courseUnitsSheets => UnmodifiableMapView(_courseUnitsSheets); @@ -23,7 +22,7 @@ class CourseUnitsInfoProvider extends StateProviderNotifier { UnmodifiableMapView> get courseUnitsClasses => UnmodifiableMapView(_courseUnitsClasses); - UnmodifiableMapView>>> + UnmodifiableMapView> get courseUnitsFiles => UnmodifiableMapView(_courseUnitsFiles); Future fetchCourseUnitSheet( 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 0602c6b88..cbbc0ded1 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -81,8 +81,8 @@ class CourseUnitDetailPageViewState tabs: [ Tab(text: S.of(context).course_info), Tab(text: S.of(context).course_class), - const Tab( - text: 'Ficheiros', + Tab( + text: S.of(context).files, ), ], ), @@ -122,7 +122,7 @@ class CourseUnitDetailPageViewState Widget _courseUnitFilesView(BuildContext context) { final sheet = context - .read() + .watch() .courseUnitsFiles[widget.courseUnit]; if (sheet == null || sheet.isEmpty) { diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart index f729fec3e..7a250dfe2 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -1,20 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:uni/model/entities/course_units/course_unit_directory.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_files_row.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_info_card.dart'; class CourseUnitFilesView extends StatelessWidget { const CourseUnitFilesView(this.files, {super.key}); - final List>> files; + final List files; @override Widget build(BuildContext context) { final cards = files - .expand( - (file) => file.entries - .where((item) => item.value.isNotEmpty) - .map((item) => _buildCard(item.key, item.value)), - ) + .where((element) => element.files.isNotEmpty) + .map((e) => _buildCard(e.folderName, e.files)) .toList(); return Container( @@ -23,12 +21,11 @@ class CourseUnitFilesView extends StatelessWidget { ); } - CourseUnitInfoCard _buildCard(String folder, List files) { - return CourseUnitInfoCard( - folder, - Column( - children: files.map(CourseUnitFilesRow.new).toList(), - ), - ); - } + CourseUnitInfoCard _buildCard(String folder, List files) => + CourseUnitInfoCard( + folder, + Column( + children: files.map(CourseUnitFilesRow.new).toList(), + ), + ); } diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index efea349fc..39fd3c33d 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/controller/networking/url_launcher.dart'; import 'package:uni/view/common_widgets/row_container.dart'; +import 'package:url_launcher/url_launcher.dart'; class ScheduleSlot extends StatelessWidget { const ScheduleSlot({ @@ -113,9 +113,9 @@ class SubjectButtonWidget extends StatelessWidget { 'UCURR_GERAL.FICHA_UC_VIEW?pv_ocorrencia_id=$occurrId'; } - Future _launchURL(BuildContext context) async { + Future _launchURL() async { final url = toUcLink(occurrId); - await launchUrlWithToast(context, url); + await launchUrl(Uri.parse(url)); } @override @@ -133,7 +133,7 @@ class SubjectButtonWidget extends StatelessWidget { color: Colors.grey, alignment: Alignment.centerRight, tooltip: 'Abrir página da UC no browser', - onPressed: () => _launchURL(context), + onPressed: () => _launchURL, ), ], ); From de8ef9ba8ebadd111566c425ef85b589850c4f06 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sat, 6 Jan 2024 20:34:27 +0000 Subject: [PATCH 15/21] Fixing minor url launcher problem --- uni/lib/view/schedule/widgets/schedule_slot.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index 39fd3c33d..a862394cb 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:uni/controller/networking/network_router.dart'; import 'package:uni/view/common_widgets/row_container.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:uni/controller/networking/url_launcher.dart'; class ScheduleSlot extends StatelessWidget { const ScheduleSlot({ @@ -113,9 +113,9 @@ class SubjectButtonWidget extends StatelessWidget { 'UCURR_GERAL.FICHA_UC_VIEW?pv_ocorrencia_id=$occurrId'; } - Future _launchURL() async { + Future _launchURL(BuildContext context) async { final url = toUcLink(occurrId); - await launchUrl(Uri.parse(url)); + await launchUrlWithToast(context, url); } @override @@ -133,7 +133,7 @@ class SubjectButtonWidget extends StatelessWidget { color: Colors.grey, alignment: Alignment.centerRight, tooltip: 'Abrir página da UC no browser', - onPressed: () => _launchURL, + onPressed: () => _launchURL(context), ), ], ); From 4da11d369aec229e7e3a47cabf5a5ac08429b15d Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sat, 6 Jan 2024 20:34:55 +0000 Subject: [PATCH 16/21] Formatting --- .../course_unit_info/widgets/course_unit_files.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart index 55ab70b84..abd1ea77f 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files.dart @@ -18,11 +18,11 @@ class CourseUnitFilesView extends StatelessWidget { return cards.isEmpty ? Center( - child: Container( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Text(S.of(context).no_files_found), - ), - ) + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Text(S.of(context).no_files_found), + ), + ) : Container( padding: const EdgeInsets.only(left: 10, right: 10), child: ListView( From 39109db50c960d7a4019fcd5718fe2a55ff8d839 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Sat, 6 Jan 2024 20:37:52 +0000 Subject: [PATCH 17/21] Lint fix --- uni/lib/view/schedule/widgets/schedule_slot.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uni/lib/view/schedule/widgets/schedule_slot.dart b/uni/lib/view/schedule/widgets/schedule_slot.dart index a862394cb..efea349fc 100644 --- a/uni/lib/view/schedule/widgets/schedule_slot.dart +++ b/uni/lib/view/schedule/widgets/schedule_slot.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:uni/controller/networking/network_router.dart'; -import 'package:uni/view/common_widgets/row_container.dart'; import 'package:uni/controller/networking/url_launcher.dart'; +import 'package:uni/view/common_widgets/row_container.dart'; class ScheduleSlot extends StatelessWidget { const ScheduleSlot({ From 633f18293f3cfa54d8861838b705ffa5393b49b5 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Mon, 8 Jan 2024 14:42:17 +0000 Subject: [PATCH 18/21] Typo and synchronous getter --- uni/lib/controller/cleanup.dart | 6 +++--- .../controller/local_storage/preferences_controller.dart | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/uni/lib/controller/cleanup.dart b/uni/lib/controller/cleanup.dart index 2e595b079..8b8c519db 100644 --- a/uni/lib/controller/cleanup.dart +++ b/uni/lib/controller/cleanup.dart @@ -43,7 +43,7 @@ Future cleanupStoredData(BuildContext context) async { } Future cleanupCachedFiles() async { - final lastCleanupDate = await PreferencesController.getLastCleanUpDate(); + final lastCleanupDate = PreferencesController.getLastCleanUpDate(); final daysSinceLastCleanup = DateTime.now().difference(lastCleanupDate).inDays; @@ -52,7 +52,7 @@ Future cleanupCachedFiles() async { } final toCleanDirectory = await getApplicationDocumentsDirectory(); - final treshold = DateTime.now().subtract(const Duration(days: 30)); + final threshold = DateTime.now().subtract(const Duration(days: 30)); final directories = toCleanDirectory.listSync(followLinks: false); for (final directory in directories) { @@ -62,7 +62,7 @@ Future cleanupCachedFiles() async { final oldFiles = files.where((file) { try { final fileDate = File(file.path).lastModifiedSync(); - return fileDate.isBefore(treshold); + return fileDate.isBefore(threshold); } catch (e) { return false; } diff --git a/uni/lib/controller/local_storage/preferences_controller.dart b/uni/lib/controller/local_storage/preferences_controller.dart index f0448a22a..8a37f12b6 100644 --- a/uni/lib/controller/local_storage/preferences_controller.dart +++ b/uni/lib/controller/local_storage/preferences_controller.dart @@ -141,12 +141,10 @@ class PreferencesController { } static Future setLastCleanUpDate(DateTime date) async { - final prefs = await SharedPreferences.getInstance(); await prefs.setString(_lastCacheCleanUpDate, date.toString()); } - static Future getLastCleanUpDate() async { - final prefs = await SharedPreferences.getInstance(); + static DateTime getLastCleanUpDate() { final date = prefs.getString(_lastCacheCleanUpDate) ?? DateTime.now().toString(); return DateTime.parse(date); From 5bf36c627cdd8806ca6f37808af20a17152caf48 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 2 Feb 2024 14:59:42 +0000 Subject: [PATCH 19/21] Solving files format --- uni/lib/controller/parsers/parser_course_unit_info.dart | 9 ++++++--- .../course_unit_info/widgets/course_unit_files_row.dart | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/uni/lib/controller/parsers/parser_course_unit_info.dart b/uni/lib/controller/parsers/parser_course_unit_info.dart index 9bf3deac7..ad8751b19 100644 --- a/uni/lib/controller/parsers/parser_course_unit_info.dart +++ b/uni/lib/controller/parsers/parser_course_unit_info.dart @@ -22,12 +22,15 @@ Future> parseFiles( final files = []; for (final file in item['ficheiros'] as List) { if (file is Map) { - final fileName = file['nome'].toString(); - final fileDate = file['data_actualizacao'].toString(); + final fileName = file['nome']; + final fileDate = file['data_actualizacao']; final fileCode = file['codigo'].toString(); + final format = file['filename'] + .toString() + .substring(file['filename'].toString().indexOf('.')); final url = await CourseUnitsInfoFetcher().getDownloadLink(session); final courseUnitFile = CourseUnitFile( - '${fileName}_$fileDate', + '${fileName}_$fileDate$format', url, fileCode, ); diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index e2e73a926..8483be5a3 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -85,12 +85,11 @@ class CourseUnitFilesRowState extends State try { final result = await loadFileFromStorageOrRetrieveNew( - '${unitFile.name}.pdf', + unitFile.name, unitFile.url, session, headers: {'pct_id': unitFile.fileCode}, ); - await OpenFile.open(result!.path); } catch (e) { if (context.mounted) { From c6b8334496b8c0ce45bf3953e25ef9ad4bc7a7ce Mon Sep 17 00:00:00 2001 From: DGoiana Date: Fri, 9 Feb 2024 15:02:28 +0000 Subject: [PATCH 20/21] Encapsulating pulse animation --- .../view/common_widgets/pulse_animation.dart | 26 +++++++++++++++++++ .../course_unit_info/course_unit_info.dart | 11 +------- .../widgets/course_unit_files_row.dart | 17 +++--------- 3 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 uni/lib/view/common_widgets/pulse_animation.dart diff --git a/uni/lib/view/common_widgets/pulse_animation.dart b/uni/lib/view/common_widgets/pulse_animation.dart new file mode 100644 index 000000000..a3b3e614c --- /dev/null +++ b/uni/lib/view/common_widgets/pulse_animation.dart @@ -0,0 +1,26 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class PulseAnimation extends StatelessWidget { + const PulseAnimation( + {super.key, required this.description, required this.controller}); + final String description; + final AnimationController controller; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, child) { + return Opacity( + opacity: 1 - 0.5 * sin(controller.value * pi), + child: Text( + description.substring(0, description.indexOf('_')), + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + }, + ); + } +} 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 6efc3fd06..c6d4d89be 100644 --- a/uni/lib/view/course_unit_info/course_unit_info.dart +++ b/uni/lib/view/course_unit_info/course_unit_info.dart @@ -125,16 +125,7 @@ class CourseUnitDetailPageViewState .watch() .courseUnitsFiles[widget.courseUnit]; - if (sheet == null || sheet.isEmpty) { - return Center( - child: Text( - S.of(context).no_info, - textAlign: TextAlign.center, - ), - ); - } - - return CourseUnitFilesView(sheet); + return CourseUnitFilesView(sheet!); } Widget _courseUnitClassesView(BuildContext context) { diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index 8483be5a3..f12ec41d9 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -5,6 +5,7 @@ import 'package:open_file_plus/open_file_plus.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/generated/l10n.dart'; +import 'package:uni/view/common_widgets/pulse_animation.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; @@ -58,19 +59,9 @@ class CourseUnitFilesRowState extends State }, child: Container( padding: const EdgeInsets.only(left: 10), - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Opacity( - opacity: 1 - 0.5 * sin(_controller.value * pi), - child: Text( - widget.file.name - .substring(0, widget.file.name.indexOf('_')), - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - ), - ); - }, + child: PulseAnimation( + description: widget.file.name, + controller: _controller, ), ), ), From 87df88ee1dbe22231d158a67cb26c780e7ac6329 Mon Sep 17 00:00:00 2001 From: DGoiana Date: Mon, 12 Feb 2024 16:33:43 +0000 Subject: [PATCH 21/21] Handling different opening results --- uni/lib/generated/intl/messages_en.dart | 10 +++ uni/lib/generated/intl/messages_pt_PT.dart | 12 +++- uni/lib/generated/l10n.dart | 58 +++++++++++++++-- uni/lib/l10n/intl_en.arb | 14 ++++- uni/lib/l10n/intl_pt_PT.arb | 14 ++++- .../view/common_widgets/pulse_animation.dart | 17 +++-- .../widgets/course_unit_files_row.dart | 63 ++++++++++++++----- 7 files changed, 153 insertions(+), 35 deletions(-) diff --git a/uni/lib/generated/intl/messages_en.dart b/uni/lib/generated/intl/messages_en.dart index f1c673060..de7261274 100644 --- a/uni/lib/generated/intl/messages_en.dart +++ b/uni/lib/generated/intl/messages_en.dart @@ -114,6 +114,8 @@ class MessageLookup extends MessageLookupByLibrary { "D. Beatriz\'s stationery store"), "dona_bia_building": MessageLookupByLibrary.simpleMessage( "Floor -1 of building B (B-142)"), + "download_error": + MessageLookupByLibrary.simpleMessage("Error downloading the file"), "ects": MessageLookupByLibrary.simpleMessage("ECTS performed: "), "edit_off": MessageLookupByLibrary.simpleMessage("Edit"), "edit_on": MessageLookupByLibrary.simpleMessage("Finish editing"), @@ -166,6 +168,8 @@ class MessageLookup extends MessageLookupByLibrary { "nav_title": m2, "news": MessageLookupByLibrary.simpleMessage("News"), "no": MessageLookupByLibrary.simpleMessage("No"), + "no_app": MessageLookupByLibrary.simpleMessage( + "No app found to open the file"), "no_bus": MessageLookupByLibrary.simpleMessage("Don\'t miss any bus!"), "no_bus_stops": MessageLookupByLibrary.simpleMessage("No configured stops"), @@ -217,12 +221,16 @@ class MessageLookup extends MessageLookupByLibrary { "occurrence_type": MessageLookupByLibrary.simpleMessage("Type of occurrence"), "of_month": MessageLookupByLibrary.simpleMessage("of"), + "open_error": + MessageLookupByLibrary.simpleMessage("Error opening the file"), "other_links": MessageLookupByLibrary.simpleMessage("Other links"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "For security reasons, passwords must be changed periodically."), "password": MessageLookupByLibrary.simpleMessage("password"), "pendent_references": MessageLookupByLibrary.simpleMessage("Pending references"), + "permission_denied": + MessageLookupByLibrary.simpleMessage("Permission denied"), "personal_assistance": MessageLookupByLibrary.simpleMessage("Face-to-face assistance"), "press_again": @@ -256,6 +264,8 @@ class MessageLookup extends MessageLookupByLibrary { "student_number": MessageLookupByLibrary.simpleMessage("student number"), "success": MessageLookupByLibrary.simpleMessage("Sent with success"), + "successful_open": + MessageLookupByLibrary.simpleMessage("File opened successfully"), "tele_assistance": MessageLookupByLibrary.simpleMessage("Telephone assistance"), "tele_personal_assistance": MessageLookupByLibrary.simpleMessage( diff --git a/uni/lib/generated/intl/messages_pt_PT.dart b/uni/lib/generated/intl/messages_pt_PT.dart index 56896f6ad..76da6d9ff 100644 --- a/uni/lib/generated/intl/messages_pt_PT.dart +++ b/uni/lib/generated/intl/messages_pt_PT.dart @@ -113,6 +113,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Papelaria D. Beatriz"), "dona_bia_building": MessageLookupByLibrary.simpleMessage( "Piso -1 do edifício B (B-142)"), + "download_error": MessageLookupByLibrary.simpleMessage( + "Erro ao descarregar o ficheiro"), "ects": MessageLookupByLibrary.simpleMessage("ECTS realizados: "), "edit_off": MessageLookupByLibrary.simpleMessage("Editar"), "edit_on": MessageLookupByLibrary.simpleMessage("Concluir edição"), @@ -153,7 +155,7 @@ class MessageLookup extends MessageLookupByLibrary { "library_occupation": MessageLookupByLibrary.simpleMessage("Ocupação da Biblioteca"), "load_error": MessageLookupByLibrary.simpleMessage( - "Aconteceu um erro ao carregar os dados"), + "Erro ao carregar a informação"), "loading_terms": MessageLookupByLibrary.simpleMessage( "Carregando os Termos e Condições..."), "login": MessageLookupByLibrary.simpleMessage("Entrar"), @@ -166,6 +168,8 @@ class MessageLookup extends MessageLookupByLibrary { "nav_title": m2, "news": MessageLookupByLibrary.simpleMessage("Notícias"), "no": MessageLookupByLibrary.simpleMessage("Não"), + "no_app": MessageLookupByLibrary.simpleMessage( + "Nenhuma aplicação encontrada para abrir o ficheiro"), "no_bus": MessageLookupByLibrary.simpleMessage( "Não percas nenhum autocarro!"), "no_bus_stops": MessageLookupByLibrary.simpleMessage( @@ -219,12 +223,16 @@ class MessageLookup extends MessageLookupByLibrary { "occurrence_type": MessageLookupByLibrary.simpleMessage("Tipo de ocorrência"), "of_month": MessageLookupByLibrary.simpleMessage("de"), + "open_error": + MessageLookupByLibrary.simpleMessage("Erro ao abrir o ficheiro"), "other_links": MessageLookupByLibrary.simpleMessage("Outros links"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente."), "password": MessageLookupByLibrary.simpleMessage("palavra-passe"), "pendent_references": MessageLookupByLibrary.simpleMessage("Referências pendentes"), + "permission_denied": + MessageLookupByLibrary.simpleMessage("Sem permissão"), "personal_assistance": MessageLookupByLibrary.simpleMessage("Atendimento presencial"), "press_again": MessageLookupByLibrary.simpleMessage( @@ -258,6 +266,8 @@ class MessageLookup extends MessageLookupByLibrary { "student_number": MessageLookupByLibrary.simpleMessage("número de estudante"), "success": MessageLookupByLibrary.simpleMessage("Enviado com sucesso"), + "successful_open": + MessageLookupByLibrary.simpleMessage("Ficheiro aberto com sucesso"), "tele_assistance": MessageLookupByLibrary.simpleMessage("Atendimento telefónico"), "tele_personal_assistance": MessageLookupByLibrary.simpleMessage( diff --git a/uni/lib/generated/l10n.dart b/uni/lib/generated/l10n.dart index 1923c90cc..30773b6a0 100644 --- a/uni/lib/generated/l10n.dart +++ b/uni/lib/generated/l10n.dart @@ -763,11 +763,11 @@ class S { ); } - /// `Error loading the information` - String get load_error { + /// `Error downloading the file` + String get download_error { return Intl.message( - 'Error loading the information', - name: 'load_error', + 'Error downloading the file', + name: 'download_error', desc: '', args: [], ); @@ -1228,6 +1228,56 @@ class S { ); } + /// `File opened successfully` + String get successful_open { + return Intl.message( + 'File opened successfully', + name: 'successful_open', + desc: '', + args: [], + ); + } + + /// `Permission denied` + String get permission_denied { + return Intl.message( + 'Permission denied', + name: 'permission_denied', + desc: '', + args: [], + ); + } + + /// `Error opening the file` + String get open_error { + return Intl.message( + 'Error opening the file', + name: 'open_error', + desc: '', + args: [], + ); + } + + /// `No app found to open the file` + String get no_app { + return Intl.message( + 'No app found to open the file', + name: 'no_app', + desc: '', + args: [], + ); + } + + /// `Error loading the information` + String get load_error { + return Intl.message( + 'Error loading the information', + name: 'load_error', + desc: '', + args: [], + ); + } + /// `Prints` String get prints { return Intl.message( diff --git a/uni/lib/l10n/intl_en.arb b/uni/lib/l10n/intl_en.arb index 4dc0cc3f1..bf8d2121c 100644 --- a/uni/lib/l10n/intl_en.arb +++ b/uni/lib/l10n/intl_en.arb @@ -150,8 +150,8 @@ }, "library_occupation": "Library Occupation", "@library_occupation": {}, - "load_error": "Error loading the information", - "@load_error": {}, + "download_error": "Error downloading the file", + "@download_error": {}, "loading_terms": "Loading Terms and Conditions...", "@loading_terms": {}, "login": "Login", @@ -240,6 +240,16 @@ "@press_again": {}, "print": "Print", "@print": {}, + "successful_open": "File opened successfully", + "@successful_open": {}, + "permission_denied": "Permission denied", + "@permission_denied": {}, + "open_error": "Error opening the file", + "@open_error": {}, + "no_app": "No app found to open the file", + "@no_app": {}, + "load_error": "Error loading the information", + "@load_error": {}, "prints": "Prints", "@prints": {}, "problem_id": "Brief identification of the problem", diff --git a/uni/lib/l10n/intl_pt_PT.arb b/uni/lib/l10n/intl_pt_PT.arb index 42cf70ce8..56dbd3945 100644 --- a/uni/lib/l10n/intl_pt_PT.arb +++ b/uni/lib/l10n/intl_pt_PT.arb @@ -150,10 +150,20 @@ "time": {} } }, + "load_error": "Erro ao carregar a informação", + "@load_error": {}, "library_occupation": "Ocupação da Biblioteca", "@library_occupation": {}, - "load_error": "Aconteceu um erro ao carregar os dados", - "@load_error": {}, + "download_error": "Erro ao descarregar o ficheiro", + "@download_error": {}, + "successful_open": "Ficheiro aberto com sucesso", + "@successful_open": {}, + "permission_denied": "Sem permissão", + "@permission_denied": {}, + "open_error": "Erro ao abrir o ficheiro", + "@open_error": {}, + "no_app": "Nenhuma aplicação encontrada para abrir o ficheiro", + "@no_app": {}, "loading_terms": "Carregando os Termos e Condições...", "@loading_terms": {}, "login": "Entrar", diff --git a/uni/lib/view/common_widgets/pulse_animation.dart b/uni/lib/view/common_widgets/pulse_animation.dart index a3b3e614c..45476c9b2 100644 --- a/uni/lib/view/common_widgets/pulse_animation.dart +++ b/uni/lib/view/common_widgets/pulse_animation.dart @@ -2,23 +2,22 @@ import 'dart:math'; import 'package:flutter/material.dart'; class PulseAnimation extends StatelessWidget { - const PulseAnimation( - {super.key, required this.description, required this.controller}); - final String description; + const PulseAnimation({ + required this.child, + required this.controller, + super.key, + }); + final Widget child; final AnimationController controller; @override Widget build(BuildContext context) { return AnimatedBuilder( animation: controller, - builder: (context, child) { + builder: (context, _) { return Opacity( opacity: 1 - 0.5 * sin(controller.value * pi), - child: Text( - description.substring(0, description.indexOf('_')), - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - ), + child: child, ); }, ); diff --git a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart index f12ec41d9..88c26c6dc 100644 --- a/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart +++ b/uni/lib/view/course_unit_info/widgets/course_unit_files_row.dart @@ -1,13 +1,13 @@ -import 'dart:math'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:open_file_plus/open_file_plus.dart'; import 'package:provider/provider.dart'; import 'package:uni/controller/local_storage/file_offline_storage.dart'; import 'package:uni/generated/l10n.dart'; -import 'package:uni/view/common_widgets/pulse_animation.dart'; import 'package:uni/model/entities/course_units/course_unit_file.dart'; import 'package:uni/model/providers/startup/session_provider.dart'; +import 'package:uni/view/common_widgets/pulse_animation.dart'; import 'package:uni/view/common_widgets/toast_message.dart'; class CourseUnitFilesRow extends StatefulWidget { @@ -60,8 +60,13 @@ class CourseUnitFilesRowState extends State child: Container( padding: const EdgeInsets.only(left: 10), child: PulseAnimation( - description: widget.file.name, controller: _controller, + child: Text( + widget.file.name + .substring(0, widget.file.name.indexOf('_')), + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), ), ), ), @@ -72,24 +77,48 @@ class CourseUnitFilesRowState extends State } Future openFile(BuildContext context, CourseUnitFile unitFile) async { - final session = context.read().state!; + final session = context.read().state; + + final result = await loadFileFromStorageOrRetrieveNew( + unitFile.name, + unitFile.url, + session, + headers: {'pct_id': unitFile.fileCode}, + ); - try { - final result = await loadFileFromStorageOrRetrieveNew( - unitFile.name, - unitFile.url, - session, - headers: {'pct_id': unitFile.fileCode}, - ); - await OpenFile.open(result!.path); - } catch (e) { + if (result?.path != null) { + final resultType = await OpenFile.open(result!.path); + if (context.mounted) handleFileOpening(resultType.type, context); + } else { if (context.mounted) { - await ToastMessage.error( - context, - S.of(context).load_error, - ); + await ToastMessage.error(context, S.of(context).download_error); } } + _controller.reset(); } + + void handleFileOpening(ResultType resultType, BuildContext context) { + switch (resultType) { + case ResultType.done: + ToastMessage.success( + context, + S.of(context).successful_open, + ); + case ResultType.error: + ToastMessage.error( + context, + S.of(context).open_error, + ); + case ResultType.noAppToOpen: + ToastMessage.warning( + context, + S.of(context).no_app, + ); + case ResultType.permissionDenied: + ToastMessage.warning(context, S.of(context).permission_denied); + case ResultType.fileNotFound: + ToastMessage.error(context, S.of(context).download_error); + } + } }